Gradle的构建的三个阶段
根据Gradle的官方文档,Gradle的构建分成三个阶段:
Gradle允许multi-project,也就是android studio里面的项目+模块的形式。安卓项目被称为Root Project,而每个模块其实都是一个子project。Root Project肯定是要参与编译的,但Gradle是怎么知道哪些子Project需要参与编译呢?其实它就是在Initialization阶段执行settings.gradle脚本得到的,这个脚本会将子Project给include进来。
每个Project都与一个build.gradle文件一一对应,build.gradle可以说就是Project的配置脚本。在配置阶段Gradle会执行所有参与编译的Project的build.gradle脚本,这个脚本对Project进行配置并创建一系列Task,这些Task之间会有依赖关系,构成一个有向无环图。
执行阶段我们可以选择一个或者多个Task去执行。被执行到的Task可能会依赖其他的Task,Gradle保证这些Task按照依赖关系的顺序执行,并且每个任务只执行一次。
我们可以通过添加打印查看settings.gradle和build.gradle被执行的时机:
1 2 3 4 5
| include ':app' rootProject.name='My Application'
println("---> settings.gradle")
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| println("---> build.gradle for Root Project")
buildscript { repositories { google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:3.5.1' } } ...
|
1 2 3 4 5 6 7 8 9
| println("---> build.gradle for app Project")
apply plugin: 'com.android.application'
android { ... } ...
|
然后执行.gradlew build查看打印:
1 2 3 4 5 6 7 8 9 10 11
| ./gradlew build ---> settings.gradle
> Configure project : ---> build.gradle for Root Project
> Configure project :app ---> build.gradle for app Project
> Task :app:lint ...
|
Project的配置
上面我们讲到每个Project都与一个build.gradle文件一一对应,每个Project都有一个Project实例,而build.gradle实际上调用的是这个实例的方法。这个实例名字是project,可以被省略,也就是说build.gradle的完整形态应该是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| project.apply([plugin: 'com.android.application'])
project.android({ compileSdkVersion 30 buildToolsVersion "30.0.2" defaultConfig { ... } buildTypes { ... } })
project.dependencies({ implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' })
|
所以build.gradle配置Project的本质其实就是对project对象调用了一系列的方法。
Task
Task表示构建的单个原子工作,例如编译类或生成javadoc,Project本质上是Task对象的集合。
我们可以通过Project的task方法创建一个Task:
1 2
| def ADemoTask = project.task("ADemoTask")
|
这样创建的Task默认分组是other:
我们可以在创建的时候给它指定分组:
1
| def ADemoTask = project.task("ADemoTask", group: "build")
|
Task是由一个Action列表组成的,Action实际上是一个闭包函数。我们可以用doFirst往列表头插入Action,也能用doLast往列表尾插入Action:
1 2 3 4 5 6 7 8 9 10
| def ADemoTask = project.task("ADemoTask", group: "build") ADemoTask.doFirst({ println("1") }) ADemoTask.doFirst({ println("2") }) ADemoTask.doLast({ println("3") }) ADemoTask.doLast({ println("4") })
|
执行这个Task,最终得到的打印是这样的:
1 2 3 4 5
| > Task :app:ADemoTask 2 1 3 4
|
Task DSL定义原理
由于task方法支持直接传入一个闭包函数进行配置,所以我们可以写成下面的方式:
1 2 3 4 5 6 7 8 9
| project.task("ADemoTask", { it.doFirst{ println("1") } it.doFirst{ println("2") } it.doLast{ println("3") } it.doLast{ println("4") } } )
|
也就是说在Configuration阶段,build.gradle脚本会调用project.task方法,将Task的配置以闭包函数的形式传入,task方法内部会去执行这个闭包。
基于groovy语法,我们可以将这个闭包移出圆括号:
1 2 3 4 5 6
| project.task("ADemoTask") { it.doFirst{ println("1") } it.doFirst{ println("2") } it.doLast{ println("3") } it.doLast{ println("4") } }
|
为了更加的简洁,我们可以省略project和it对象,然后把task方法函数调用的圆括号也省略:
1 2 3 4 5 6
| task "ADemoTask" { doFirst{ println("1") } doFirst{ println("2") } doLast{ println("3") } doLast{ println("4") } }
|
Gradle在处理build.gradle的时候会有一个转换,将task identifier arg转换成task “identifier” arg(下面的是Gladle的源码从知乎上看到的):
1 2 3 4 5 6 7 8
| if (args.getExpression(0) instanceof MapExpression && args.getExpression(1) instanceof VariableExpression) { transformVariableExpression(call, 1); } else if (args.getExpression(0) instanceof VariableExpression) { transformVariableExpression(call, 0); }
|
也就是说我们的task名称的双引号也是可以省略的,虽然这不属于groovy的语法糖,但在构建的时候Gradle会对我们的代码进行转换。于是就变成了我们常见的task定义方式:
1 2 3 4 5 6 7
| task ADemoTask { println("ADemoTask...") doFirst{ println("1") } doFirst{ println("2") } doLast{ println("3") } doLast{ println("4") } }
|
所以ADemoTask后方的闭包内的代码(如println)在配置阶段执行,而doFirst/doLast后方的闭包实际是Action,会在执行阶段执行。
Task的依赖关系
如同第一节所说Task之间是会有依赖关系的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| task TaskA { doLast { println("TaskA run...") } }
task TaskB { doLast { println("TaskB run...") } }
task TaskC { doLast { println("TaskC run...") } }
TaskC.dependsOn TaskA TaskC.dependsOn TaskB
|
task方法为project对象拓展了Task成员变量,我们可以调用Task的dependsOn方法给它添加依赖,Gradle在执行一个Task之前,会先执行它所依赖的Task:
1 2 3 4 5 6 7 8 9 10
| ./gradlew TaskC
> Task :app:TaskA TaskA run...
> Task :app:TaskB TaskB run...
> Task :app:TaskC TaskC run...
|
增量编译
编译一般情况下是通过一些输入的文件来执行编译动作,然后输出另外的文件。在输入文件没有改变的情况下,实际上不需要每次都执行边编译:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| task ADemoTask { def srcFile = new File("src.txt") def destFile = new File("dest.txt")
inputs.file(srcFile) outputs.file(destFile)
doLast { println("run...") destFile.delete() srcFile.eachLine {line -> destFile.append("${line}\n") } } }
|
像上面的例子我们指定了task的inputs和outputs,只要outputs已经被生成,且inputs没有修改,那么doLast加入的Action就不会被执行。
Project、Task、Action间的关系
经过上面的讲解我们大概可以理解Project、Task、Action之间的关系大概如下图:
Project间存在依赖,每个Project包含多个存在依赖的Task,Task内部有一个Action列表,Task的执行实际上就是内部Action的顺序执行。
插件
让我们回到build.gradle脚本,里面默认调用了project的apply、android、dependencies方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| project.apply([plugin: 'com.android.application'])
project.android({ compileSdkVersion 30 buildToolsVersion "30.0.2" defaultConfig { ... } buildTypes { ... } })
project.dependencies({ implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' })
|
dependencies方法配置了Project的依赖,Project可以依赖编译好的库,也能依赖其他的Project(android studio里面的表现是module间的依赖)。
android方法配置了一些安卓相关的配置项,但是实际上Gradle并不是专门给安卓使用的,为什么Project里面会有android这个方法呢?
答案就在apply方法传入的插件。
Gradle执行com.android.application这个插件的时候会将Project的对象传给插件,插件内部就能为这个Project新增一些拓展方法和Task。我们熟悉的build、assemble、assembleDebug这些Task都是在这个插件里面定义的。
插件的原理我就不细讲了,刚兴趣的同学可以看这篇笔记,这里只简单举个添加拓展方法的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
|
class MyExt { String data1 String data2
MyExt() { this.data1 = null this.data2 = null }
void data1(String data1) { this.data1 = data1 }
void data2(String data2) { this.data2 = data2 } }
MyExt ext = new MyExt()
project.extensions.add("myExt", ext)
myExt { data1("111") data2("222") }
println(myExt.data1) println(myExt.data2)
|