从Gradle生命周期到自定义Task挂接到Build构建流程全解
Gradle构建工具非常的灵活,它提供了一系列的Api让我们有能力去修改或定制项目的构建过程.
我们知道Gradle构建工具非常的灵活,它提供了一系列的Api让我们有能力去修改或定制项目的构建过程,在项目的编译过程中,插入我们自己的 Task 并执行相关的操作,如:多渠道打包,ASM代码织入和资源的检测等。
要想实现这些功能,首先就需要明白Gradle的构建流程,知道Gradle在每个阶段都做了什么,加上自己需要在哪个阶段做什么事件,就可以通过Gradle提供的Api,插入我们想要执行的代码。因此理解Gradle的生命周期和Hook点,有助于我们梳理、扩展项目的构建流程。
Gradle的构建过程有着固定的生命周期,理解Gradle的生命周期和Hook点,有助于帮你梳理、扩展项目的构建流程。
一、Gradle 构建生命周期
Gradle的构建过程有着固定的生命周期,分别是:
- 初始化阶段
- 配置阶段
- 执行阶段
下面就详细介绍一下这三个阶段都做了什么事情。
1、初始化阶段
初始化阶段的主要任务:就是创建项目的层次结构,并为每一个项目创建一个Project 实例对象。
在初始化阶段中,会执行 settings.gradle 脚本,并读取 settings.gradle 中的 include 信息,进而为每一个工程(即build.gradle 脚本文件)创建一个与之对应的 Project 对象,最终形成一个项目的层次结构。 一个 settings.gradle 脚本对应一个 Settings 对象(在Gradle 初始化的时候创建),而我们最常用来声明项目的层次结构的 include标签 就是 Settings 类下的一个方法,Settings 类还有如下方法,都可以直接在 settings.gradle 文件中直接访问:
1-1、应用其它工程的模块
比如:通过 include 和 project 方法,可以实现引用任何位置下的工程模块:
include ':myjson' // 需要参与构建的模块名称
project(':myjson').projectDir = file('/Users/WorkSpace/AndroidDemo/MyJson/myjson')
- include方法 :指定参与构建的模块的名称,模块名前需要加冒号(:),模块名称可以任取。
- project方法:加载指定的模块,并为该模块设置一个工程路径,参数必须与include参数一致 。
这样本工程就可以引用到其它位置的模块了,如果引用的是一个 library 模块,那么在本工程的模块中,就可以依赖这个 library 模块了,如:
implementation project(":myjson")
1-2、监听初始化过程
在上面的 Settings 类的方法列表中,有一个 getGradle() 方法,返回一个 Gradle 对象,通过这个 Gradle 对象,我们可以监听 Gradle 构建过程中 各个生命周期方法的回调,如在 settings.gradle 文件中,增加如下监听:
gradle.addBuildListener(new BuildListener() {
void buildStarted(Gradle var1) {
println 'buildStarted()->开始构建'
}
void settingsEvaluated(Settings var1) {
println 'settingsEvaluated()->settings评估完成(settins.gradle中代码执行完毕)'
// var1.gradle.rootProject 这里访问Project对象时会报错,还未完成Project的初始化
}
void projectsLoaded(Gradle var1) {
println 'projectsLoaded()->项目结构加载完成(初始化阶段结束)'
println 'projectsLoaded()->初始化结束,可访问根项目:' + var1.gradle.rootProject
}
void projectsEvaluated(Gradle var1) {
println 'projectsEvaluated()->所有项目评估完成(配置阶段结束)'
}
void buildFinished(BuildResult var1) {
println 'buildFinished()->构建结束 '
}
})
在 build.gradle 文件中,也可以拿到 Gradle 对象,但如果在 build.gradle 文件中添加上面的监听事件的话,buildStarted,settingsEvaluated 和 projectsLoaded 方法是不会回调的,因为这三个方法是在初始化阶段 执行 settings.gradle 文件的时候执行的,但另外两个方法是会回调的。
在根工程的 build.gradle 文件中,增加如下代码,以便更好的观察上面添加的监听事件的回调时机:
allprojects {
afterEvaluate {
println "${name}:配置完成"
}
}
打印信息如下:
settingsEvaluated()->settings评估完成(settins.gradle中代码执行完毕)
projectsLoaded()->项目结构加载完成(初始化阶段结束)
projectsLoaded()->初始化结束,可访问根项目:root project 'KotlinLearning'
Configure project :
KotlinLearning:配置完成
Configure project :app
app配置完成
Configure project :kotlinlearning
kotlinlearning配置完成
projectsEvaluated()->所有项目评估完成(配置阶段结束)
buildFinished()->构建结束
2、配置阶段
配置阶段的任务:是执行各项目下的build.gradle脚本,完成 Project 的配置,并且构造Task任务依赖关系图以便在执行阶段按照依赖关系执行Task。
2-1、配置阶段执行的代码
配置阶段也是我们最常接触到的构建阶段,比如应用外部构建插件apply plugin: 'com.android.application',配置插件的属性android{ compileSdkVersion 25 ...}等。
每个build.gralde脚本文件对应一个Project对象,在初始化阶段创建,Project的接口文档。 配置阶段执行的代码包括:
- build.gralde中的各种语句
- 闭包
- Task中的配置段语句
验证:在根目录的build.gradle中添加如下代码:
println 'build.gradle的配置阶段'
// 调用Project的dependencies(Closure c)声明项目依赖
dependencies {
// 闭包中执行的代码
println 'dependencies中执行的代码'
}
// 创建一个Task
task test() {
println 'Task中的配置代码'
// 定义一个闭包
def a = {
println 'Task中的配置代码2'
}
// 执行闭包
a()
doFirst {
println '这段代码配置阶段不执行'
}
}
println '我是顺序执行的'
打印信息如下:
build.gradle的配置阶段
dependencies中执行的代码
Task中的配置代码 Task中的配置代码2 我是顺序执行的
从上面的打印信息可以看出,在执行了dependencies的闭包后,直接执行的是任务test中的配置段代码(Task中除了Action外的代码段都在配置阶段执行)
需要注意的是,执行任何 Gradle 命令,在初始化阶段和配置阶段的代码都会被执行。
2-2、Task 依赖关系配置完成监听
配置阶段另外一个重要的任务就是 构建 Task 依赖关系的有向无环图,说简单一点,就是给 所有的 Task 排一个执行顺序,在执行阶段的时候,就按照这个顺序去执行所有的 Task 任务。
我们可以通过 Gradle 对象的 getTaskGraph 方法来得到该有向无环图对象: TaskExecutionGraph,通过TaskExecutionGraph类的相关方法可以监听 Task 依赖关系构建完成的通知,如:
- void whenReady(Closure var1)
- void addTaskExecutionGraphListener(TaskExecutionGraphListener var1)
在 build.gradle 文件中,增加如下代码:
gradle.getTaskGraph().whenReady {
println "whenReady Task依赖关系构建完成,size=${it.allTasks.size()}"
it.allTasks.forEach { task ->
println "${task.name}"
}
}
gradle.getTaskGraph().addTaskExecutionGraphListener(new TaskExecutionGraphListener() {
@Override
void graphPopulated(TaskExecutionGraph graph) {
println "graphPopulated Task依赖关系构建完成 size=${graph.allTasks.size()}"
graph.allTasks.forEach { task ->
println "${task.name}"
}
}
})
点击运行按钮,运行app的时候,就可以打印出 Task 任务列表:
>whenReady Task依赖关系构建完成,size=40 preBuild preDebugBuild compileDebugAidl compileDebugRenderscript generateDebugBuildConfig checkDebugAarMetadata generateDebugResValues generateDebugResources mergeDebugResources createDebugCompatibleScreenManifests extractDeepLinksDebug processDebugMainManifest processDebugManifest processDebugManifestForPackage processDebugResources compileDebugKotlin javaPreCompileDebug compileDebugJavaWithJavac compileDebugSources mergeDebugNativeDebugMetadata mergeDebugShaders compileDebugShaders generateDebugAssets mergeDebugAssets compressDebugAssets processDebugJavaRes mergeDebugJavaResource checkDebugDuplicateClasses dexBuilderDebug desugarDebugFileDependencies mergeExtDexDebug mergeDexDebug mergeDebugJniLibFolders mergeDebugNativeLibs stripDebugDebugSymbols validateSigningDebug writeDebugAppMetadata writeDebugSigningConfigVersions packageDebug assembleDebug graphPopulated Task依赖关系构建完成 size=40 preBuild 。。。。。 assembleDebug
3、执行阶段
执行阶段就是根据配置阶段构建的 Task 依赖关系去执行相关的 Task。
当我们运行项目的时候,Gradle 就会根据 Task的依赖关系依次去执行相关的Task。还可以通过 gradle 命令去执行指定的 Task,如在控制台中输入如下命令,就可以执行 build 任务:
./gradlew build
build 是任务名称。
二、Gradle Hook点
Gradle 提供了非常多的接口回调以便我们修改构建过程中的行为,整体流程如下图所示:
方法说明:
- Gradle#settingsEvaluated 方法:与BuildListener的settingsEvaluated的执行时机一样,需要在 settings.gradle 文件中添加,否则无效。
- Gradle#projectsLoaded 方法:与BuildListener的projectsLoaded的执行时机一样,需要在 settings.gradle 文件中添加,否则无效。
- Gradle#beforeProject方法,在每个工程 配置之前执行,
- Project#beforeEvaluate方法,在调用该方法的 Project 配置之前执行,如果在 module 的 build.gradle 文件中调用该方法,是不会执行的,因为执行到build.gradle的时候,已经过了beforeEvaluate执行的时间节点了。
- Gradle#afterProject方法,在每个工程配置完成之后,执行该方法,哪怕构造过程出错,也会调用,截止到出错的文件。
- Project#afterEvaluate方法,在调用该方法的Project 配置执行完成之后调用,在这个时机,该工程就配置完成了,如在这个方法回调中就可以获取到所有的Task任务了。
- Gradle#projectsEvaluate方法,当所有的工程都配置完成之后,执行该方法,与BuildListener的projectsEvaluated方法的执行时机是一样的。
三、指定 Task 执行顺序
在Gradle中,有三种方式指定task的执行顺序:
- dependsOn 强依赖方式
- 通过Task输入输出
- 通过API指定执行顺序
1、通过dependsOn强依赖方式指定
dependsOn 强依赖的方式可以细分为 静态依赖 和 动态依赖:
- 静态指定依赖:在创建task的时候,就明确的知道定义的 Task需要依赖的task是什么,直接通过 dependsOn 参数或者 dependsOn 方法指定所依赖的Task。
task提供了dependsOn,finalizedBy方法来管理task之间的依赖关系,依赖关系表达的是执行这个task时所需要依赖的其他task,也就是说这个task不能被单独执行,执行这个 Task 之前或之后需要执行另外的task。 - 动态指定依赖:在创建 Task的时候,不知道需要依赖哪些 Task,通过 dependsOn 方法动态依赖符合条件的 Task。
示例如下所示:
静态指定依赖
在定义 Task 的时候,直接为 Task 指定 dependsOn 参数,值为 另一个被依赖的 Task 的名字:
task taskX {
doLast{
println 'taskX'
}
}
task taskY {
doLast{
println 'taskY'
}
}
task taskZ(dependsOn:taskX) { // 多依赖方式:dependsOn:[taskX,taskY]
doLast{
println 'taskZ'
}
}
// 当我们执行taskZ的时候,由于依赖了taskX,则taskX会先执行,然后才会执行:taskZ
除了在 Task 定义的时候指定 Task 的依赖之外,还可以通过 Task 的 dependsOn 方法,为 Task 指定依赖:
task taskZ {// 定义Task的时候不指定依赖
doLast{
println 'taskZ'
}
}
// 通过task的dependsOn方法,也可以指定task的依赖task。
taskZ.dependsOn(taskX,taskY)
当一个task依赖多个task的时候,被依赖的task之间,如果没有依赖关系的话,那么他们的执行顺序是随机的,并无影响。
动态添加依赖
当 Task 在定义的时候,不知道所依赖的 Task 是什么,在配置阶段,通过条件找出符合条件的 Task ,并进行依赖。
task lib1 {
doLask{
println 'lib1'
}
}
task lib2 {
doLask{
println 'lib2'
}
}
task lib3 {
doLask{
println 'lib3'
}
}
// 动态指定taskX依赖所有以lib开头的task
task taskX{
// 动态指定依赖
dependsOn this.tasks.findAll{ task->
return task.name.startsWidth('lib')
}
doLast {
println 'taskZ'
}
}
2、通过Task输入输出指定
当一个参数作为TaskA的输出参数,同时又作为TaskB的输入参数。那么当执行TaskB的时候先要执行TaskA。即输出的Task先于输入的Task执行。 如:
ext {
testFile = file("${this.buildDir}/test.txt")
}
// 生产者 Task
task producer {
outputs.file testFile
doLast {
outputs.getFiles().singleFile.withWriter { writer ->
writer.append("我爱中国")
}
println "producer Task 执行结束"
}
}
// 消费者 Task
task consumer {
inputs.file testFile
doLast {
println "读取文件内容:${inputs.files.singleFile.text}"
println "consumer Task 执行结束"
}
}
task testTask(dependsOn: [producer, consumer]) {
doLast {
println "测试Task执行结束"
}
}
文件 testFile 是 producer 的输出参数,是 consumer 的输入参数,所以 producer 优先于 consumer 执行。
3、通过API指定执行顺序
可以指定Task执行顺序的方法还有:
- mustRunAfter:指定必须在哪个Task执行完成之后在执行,如 taskA.mustRunAfter(taskB),表示 taskA 必须在 taskB 之后执行。
- shouldRunAfter:跟mustRunAfter类似,区别在于不强制。不常用。
- finalizedBy :在任务结束之后执行指定的 Task。如:taskA.finalizedBy(taskB),表示在 taskA 执行完成之后,再执行 taskB 任务。
示例代码:
通过mustRunAfter 指定task执行顺序:
task taskA {
doLast {
println "taskA 执行"
}
}
task taskB {
mustRunAfter(taskA)
doLast {
println "taskB 执行"
}
}
task testAB(dependsOn: [taskA, taskB]) {
doLast {
println "testAB 执行"
}
}
执行 任务:testAB,输出信息为:
shouldRunAfter与mustRunAfter类似,这就不在测试了。
通过 finalizedBy 指定 Task 的执行顺序:
task taskA {
doLast {
println "taskA 执行"
}
}
task taskB {
finalizedBy(taskA)
doLast {
println "taskB 执行"
}
}
task testAB(dependsOn: [taskA, taskB]) {
doLast {
println "testAB 执行"
}
}
同样的代码,就把 mustRunAfter 替换成了 finalizedBy 方法,按照finalizedBy 方法的含义,就是:taskB 执行结束后,再执行 taskA。运行 testAB 任务,输出信息为:
这与我们想象的结果一致。
四、自定义 Task 挂接到构建流程
1、打印 Task 依赖关系
如果对一个构建流程的任务依赖关系不熟悉的话,可以使用第三方插件来查看,在根项目的build.gradle中添加如下代码:
buildscript {
repositories {
maven {
url "https://plugins.gradle.org/m2/"
}
}
dependencies {
classpath "gradle.plugin.com.dorongold.plugins:task-tree:1.5"
}
}
// 应用插件
apply plugin: com.dorongold.gradle.tasktree.TaskTreePlugin
然后执行 ./gradlew <任务名> taskTree --no-repeat,即可看到指定Task的依赖关系,比如在Android构建中查看 app:assembleDebug任务的依赖关系:
2、自定义 Task 挂机到构建流程
我们知道,Gradle 的构建流程是通过执行一系列的 Task 任务来完成的,每一个Task完成自己独特的工作之后,就根据Task的依赖关系,执行下一个 Task 任务。如:preBuild(开始构建之前执行的Task任务)->mergeDebugResources(合并资源文件的Task任务)->assembleDebug(生成debug包的任务)。
如果想把自己写的Task也插入到构建流程中,在运行的时候自动执行我们的Task任务,又该如何做呢?这个时候就可以通过指定Task的执行顺序,把我们的 Task 加入到构建流程中,具体来说就是:明确自己的任务需要插入到哪个任务之后或者之前,接着找到这个任务,并把自己的任务插入到这个任务的前面或者后面。
可以通过如下方式,把自定义的 Task 插入到指定的任务
- 通过 dependsOn 或者 finalizedBy 方法指定
- 单独使用dependsOn 方法,最好是让编译流程的 Task 依赖于自己的 Task,否则不生效
- finalizedBy :可以单独使用,指定在某个 Task 执行结束之后,执行自己的 Task
- 通过 mustRunAfter 结合 dependsOn 一起指定
举例说明:
2-1、在 某个 Task 之前执行 自定义 Task:dependsOn
afterEvaluate {
// 1\. 找到需要依赖自己 Task的构建流程的Task
def mergeResourcesTask = tasks.findByName("mergeDebugResources")
println "mergeResourcesTask=$mergeResourcesTask"
// 2\. 通过dependsOn 方法,插入到指定Task之前
mergeResourcesTask.dependsOn(checkBigImage)
}
插入自定义 Task 之前的 Task 依赖关系图:
插入自定义 Task 之后的 Task 依赖关系图:
从Task的依赖关系图可以看出,mergeDebugResources 任务确实是依赖于checkBigImage任务了,这样在运行编译app的时候,就会在执行 mergeDebugResources 任务的时候,先去执行 checkBigImage 任务了。
2-2、在某个 Task 之后,执行自定义的Task:finizedBy
afterEvaluate {
// 1\. 找到需要依赖自己 Task的构建流程的Task
def mergeResourcesTask = tasks.findByName("mergeDebugResources")
println "mergeResourcesTask=$mergeResourcesTask"
// 2\. 通过 finalizedBy 方法,插入到指定Task之后
mergeResourcesTask.finalizedBy(checkBigImage)
}
2-3、在两个 Task 之间,插入自定义的 Task :mustRunAfter 结合 dependsOn
afterEvaluate {
// 1\. 找到需要挂接的Task
def mergeResourcesTask = tasks.findByName("mergeDebugResources")
def processDebugResourcesTask = tasks.findByName("processDebugResources")
// 2.让自定义的 Task 在 mergeDebugResources 任务之后且在 processDebugResources 之前执行
checkBigImage.mustRunAfter(mergeResourcesTask)
processDebugResourcesTask.dependsOn(checkBigImage)
}
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)