写给 Android 开发者的 Gradle 系列(二)撰写 task

欢迎关注本人公众号,扫描下方二维码或搜索公众号 id: mxszgghtml

本文基于 Android Gradle plugin 3.0.1java

前言

task 至关于开发者平常开发中所接触到的函数、方法,它们是相同的一个概念。在前文写给 Android 开发者的 Gradle 系列(一)基本姿式已经提到过 task 的概念,例如 transformClassesAndResourcesWithProguardForRelease task 是为了混淆 release 包中源码的。android

接下来就开始实操,首先在 app/build.gradle 中添加以下依赖:shell

compileOnly 'com.android.tools.build:gradle:3.0.1'api

便可在 External Libraries 中看到关于 Gradle plugin 的源码。bash

这里写图片描述

Gradle 源码如何引入将会在下一节中介绍。微信

task 撰写

task 声明

根据官方文档Task#create() 能够知道,task 的基本写法能够是以下四种:闭包

task myTask
task myTask { configure closure }
task (myTask) { configure closure }
task (name: myTask) { configure closure }
复制代码

每个 task 都有本身的名字,这样开发者才能调用它,例如调用上面的 task:app

./gradlew myTaskide

可是有一个问题,假若当前项目的 app module 和 a module 都含有一个名为 myTask 的 task,那么会不会起冲突,该如何调用它们?答案是不会冲突,调用方式以下:

./gradlew app:myTask(调用 app module 的 myTask)

./gradlew a:myTask(调用 a module 的 myTask)

经过 ProjectName:taskName 的形式即可以指定惟一绝对路径去调用指定 Project 的指定 task 了。

扩展

根据 Task#create() 能够知道,task 的建立是能够声明参数的,除了上述的 name 参数以外,还有以下几种:

  • type:默认为 DefaultTask。相似于父类。在后文中将会说起该参数。

  • dependsOn:默认为[]。但愿依赖的 tasks,等同于 Task.dependsOn(Object... path) 中的 path。在后文中将会说起该参数。

  • action:默认为 null。等同于 Task.doFirst { Action } 中的 Action。

    task (name: actionTest, action: new Action<Task>() {
      @Override
      void execute(Task task) {
        println 'hello'
      }
    }) {
    }
    复制代码

    等同于

    task (name: actionTest) {
    	doFirst {
    		println 'hello'
    	}
    }
    复制代码
  • override:默认为 false。是否替换已存在的 task。

  • group:默认为 null。task 的分组类型。

  • description:默认为 null。task 描述。

  • constructorArgs:默认为 null。传给 task 构造函数的参数。

后面四种大部分开发过程当中应该不怎么会用到,有须要的读者自行查阅文档。

task 内容格式

  1. 根据官方文档以及前一篇文章中能够知道,若是想给 task 添加操做,能够添加在 doLast {}/doFirst {} 等闭包中,例如:

    task myTask {
    	doFirst {
    		println 'myTask 最早执行的内容'
    	}
    	doLast {
    		println 'myTask 最后执行的内容'
    	}
    	// warning
    	// println 'Configuration 阶段和 Execution 阶段皆会执行'
    }
    复制代码

切记大部分的内容是写在 doLast{}doFirst{} 闭包中,由于写在若是写在 task 闭包中的话,会在 Configuration 阶段也被执行。

  1. 根据官方文档可知,为了提升 task 复用性,Gradle 还支持 Task 类的书写——

2.1 将下述代码写在 build.gradle 中,并用 @TaskAction 标记想要执行的方法。

class GreetingTask extends DefaultTask {
	String greeting = 'hello from GreetingTask'
	
	@TaskAction
    def greet() {
        println greeting
    }
}
复制代码

2.2 在 build.gradle 中撰写 task 调用 GreetingTask 类:

// Use the default greeting
task (name: hello , type: GreetingTask)

// Customize the greeting
task (name: greeting , type: GreetingTask) {
	greeting = 'greetings from GreetingTask'
}
复制代码

2.3 调用该 task——

./gradlew hello

> Task :app:hello
hello from GreetingTask

./gradlew greeting

> Task :app:greeting
greetings from GreetingTask
复制代码

因此看到这里应该不只可以理解 Task 类的书写,而且应该可以大体明白 type 这个参数的含义了。

不知道会不会和笔者同样事儿逼的读者此时会疑惑 @TaskAction 修饰的方法和 doLast {} 以及 doFirst {} 闭包的执行顺序是怎样的?

task (name: hello, type: GreetingTask) {
  doFirst {
    def list = getActions()
    for (int i = 0; i < list.size(); i++) {
      println list.get(i).displayName
    }
  }

  doLast {

  }
}
复制代码

首先声明 doFirst {}doLast {} 闭包;而后戳进 DefaultTask 源码并追踪到顶级父类 AbstractTask 中能够看到内部经过使用 actions 存储全部执行的 Action,并经过 getAction() 暴露;actions 是 List 类型,内部的元素类型是 ContextAwareTaskAction,该接口又实现了 Describable,Describable 仅声明了一个 getDisplayName() 方法,因此能够直接经过 displayName 获取该 Action 的名称。

理解以上三步便可完成上述 task 撰写,在命令行中试试——

./gradlew hello

> Task :app:hello
Execute doFirst {} action
Execute greet
Execute doLast {} action
复制代码

Gradle 内部将会自动为变量设置 setter、getter 方法,因此当一个 Gradle 有 getXxx() 方法时,能够直接使用 xxx 变量。若是不清楚这个细节,建议回顾上一篇文章的附录。

task 依赖关系

开发者常使用 dependsOn 来指定依赖关系(另外两种是指定 task 执行顺序,详见文档 Task Dependencies and Task Ordering),以下:

task a {
	doLast {
		println 'a'
	}
}

task b {
	dependsOn('a')
	doFirst {
		println 'b'
	}
}
复制代码

不妨将以上代码写在 app build.gradle 文件下,当执行 task b 的时候,会输出以下信息:

./gradlew task app:b

> Task :app:a

a

> Task :app:b

b

能够看到,因为 task b 须要依赖 task a,因此 task b 执行的时候会先执行 task a。

有经验的开发者若是在命令行中试过 assembleDebug 等命令会发现,它们的执行将会依赖于许多其余 task。因此不妨在命令行中试试 ./gradlew assembleDebug 观察输出结果。

task 实战

install && launch apk

com.android.application 自带 installDebug task,开发者可使用 installDebug 安装当前项目 apk:

./gradlew installDebug

> Task :app:installDebug

Installing APK 'app-debug.apk' on 'xxxxx' for app:debug Installed on 1 device.

可是彷佛看起来有些不尽人意的地方,例如开发者但愿安装的时候可以顺带可以启动该 app。那么该如何作呢?

首先从问题的可行性上来进行分析,开发者的直觉告诉咱们是能够经过 gradle 实现的——命令行能够安装、启动 apk——adb install -r app-debug.apkadb shell am start -n 包名/首 Activity。因此关键点就是如何经过 gradle 调用命令行代码以及如何获取到 包名/首 Activity 信息。

  1. 开发者的直觉一样告诉咱们 Gradle 开发文档中有关于命令行调用的信息,只须要使用 exec {} 闭包就行了。

  2. 如何获取 包名/首 Activity 信息?能够经过 AndroidManifest.xml 来获取。部分经验丰富的开发者知道——打入 apk 中的 AndroidManifest.xml 文件并非咱们日常写的 AndroidManifest.xml,而是 apk 编译后位于 Project/app/build/intermediates/manifests/full/debug/ 包下的 AndroidManifest.xml(固然,若是是 Release 包的话,应该是 Project/app/build/intermediates/manifests/full/release/ 包下)。

    • 包名就是 android 闭包下的 defaultConfig 闭包下的 applicationId

      这里写图片描述

    • 目标 Activity 则是包含 action 为 android.intent.action.MAIN 的 Activity。

      这里写图片描述

理解了以上内容,便不难理解下面的内容:

task installAndRun(dependsOn: 'assembleDebug') {
  doFirst {
    exec {
      workingDir "${buildDir}/outputs/apk/debug"
      commandLine 'adb', 'install', '-r', 'app-debug.apk'
    }
    exec {
      def path = "${buildDir}/intermediates/manifests/full/debug/AndroidManifest.xml"
      // xml 解析
      def parser = new XmlParser(false, false).parse(new File(path))
      // application 下的每个 activity 结点
      parser.application.activity.each { activity ->
        // activity 下的每个 intent-filter 结点
        activity.'intent-filter'.each { filter ->
          // intent-filter 下的 action 结点中的 @android:name 包含 android.intent.action.MAIN
          if (filter.action.@"android:name".contains("android.intent.action.MAIN")) {
            def targetActivity = activity.@"android:name"
            commandLine 'adb', 'shell', 'am', 'start', '-n',
                "${android.defaultConfig.applicationId}/${targetActivity}"
          }
        }
      }
    }
  }
}
复制代码
  1. install apk 的前提必须是得有一个 apk,因此势必须要依赖 assembleDebug task。

    实际上 installDebug task 也是依赖 assembleDebug task 的,不妨能够试试——

    task showInstallDepends {
      doFirst {
        println project.tasks.findByName("installDebug").dependsOn
      }
    }
    复制代码

    ./gradlew showInstallDepends

    > Configure project :app

    [task 'installDebug' input files, assembleDebug]

  2. exec 闭包中的几个参数说起下——

    2.1 workingDir:工做环境,参数为 File 格式。默认为当前 project 目录。

    2.2 commandLine:须要命令行执行的命令,参数为 List 格式。

  3. 前一篇文章中提到 ——

    说白了它们其实就是一些闭包、一些固定格式,正是由于它们的格式是固定的,task 才可以读取到相应的数据完成相应的事情。

    在第二个 exec 闭包的第八行就很好的体现了这一点,经过 {android.defaultConfig.applicationId} 直接获取到 Gradle 文件中 android 闭包下的 defaultConfig 闭包下的 applicationId 的值。由此就得到了当前应用的包名。

    固然,除了 Gradle 可以调用命令行之外,实际上 groovy 也是能够调用命令行的,但在此就不作扩展了。

  4. 至于最早启动的 Activity,确定是 action 为 android.intent.action.MAIN 的 Activity,那么问题就是变成如何在 AndroidManifest.xml 中寻找到该 Activity 的事了——做为一个合格的老司机,应该可以想到 groovy 必定会提供相应的 xml 解析 API 的,至于具体的使用笔者就不在此扩展了,留给各位读者去源码中探索成长。

  5. 除去上面的信息之外,还须要什么?还须要知道一些 gradle 构建的信息——例如 debug 包会最终出如今 ${buildDir}/outputs/apk/debug;例如 debug 包中的 AndroidManifest.xml 并非平常开发中写的那个 AndroidManifest.xml(虽然可能它俩基本没什么差别),而是 ${buildDir}/intermediates/manifests/full/debug 下的 AndroidManifest.xml。因此一是但愿各位读者平常多去翻翻 build 文件夹,二是要知道 ${buildDir}(build 文件夹)有多么重要,由于 Gradle 构建 apk 的过程当中,但凡是有输出文件那么基本都会存在这个文件夹中,因此多去翻一翻。

由此以后,能够在命令行输入如下命令:

./gradlew installAndRun

> Task :app:installAndRun

[ 4%] /data/local/tmp/app-debug.apk

[ 8%] /data/local/tmp/app-debug.apk

[ 12%] /data/local/tmp/app-debug.apk ...

Success

Starting: Intent {com.test.Test/TestActivity}

至此便完成了一个安装并启动 apk 的 task 撰写了。

hook assets

上面的 task 看起来彷佛和 Android 的构建过程并没有多大关系,没错,那么接下来不妨深层次接触试试——经过 hook 原生 task 实现更改打包中的文件——在打包过程当中向 assets 插入一张图片。

尽管这看起来丝毫没卵用

在打包流程中,有一个 task 名为 packageDebug,该 task 是打包文件生成 apk 的——

这里写图片描述

接着,不妨在命令行键入如下命令:

./gradlew help --task "packageDebug"

Type

PackageApplication (com.android.build.gradle.tasks.PackageApplication)

能够看到,该 task 的 type 是 PackageApplication——

这里写图片描述

不妨再看看它的父类 PackageAndroidArtifact

这里写图片描述

看到关键信息,该 task 中有一个类型为 FileCollection assets 字段,这即是最终打入 apk 中的那个 assets 了。因此不难写出如下代码——

task hookAssets {
  afterEvaluate {
    tasks.findByName("packageDebug").doFirst { task ->
      copy {
        from "${projectDir.absolutePath}/test.png"
        into "${task.assets.asPath}"
      }
    }
  }
}
复制代码
  1. 在 project afterEvaluate 以后找到 packageDebug task
  2. 不妨在 app 目录下放入一个 test.png,使用 copy {} 闭包,from 填入的参数为 test.png 的路径,into 填入的参数为输出的路径,也就是 assets 的路径。

能够看到 /app/build/intermediates/assets/debug/ 下存有 test.png

这里写图片描述

一样地,解压 apk 文件也能够看到——

这里写图片描述

一个实打实的 Gradle task hook 流程就这么操做完了。

后记

若是说 Gradle task 是函数的话,那么 Gradle plugin 就是函数库,在后一节笔者将会对 Gradle plugin 进行阐述。

固然,若是各位读者有疑问的话,欢迎加入笔者的微信群。 若是二维码失效,能够查看笔者最新文章的尾部。

这里写图片描述
相关文章
相关标签/搜索