盒子
盒子
文章目录
  1. Gradle的构建的三个阶段
  2. Project的配置
  3. Task
    1. Task DSL定义原理
    2. Task的依赖关系
    3. 增量编译
  4. Project、Task、Action间的关系
  5. 插件

Gradle构建原理

Gradle的构建的三个阶段

根据Gradle的官方文档,Gradle的构建分成三个阶段:

  • Initialization (初始化)

Gradle允许multi-project,也就是android studio里面的项目+模块的形式。安卓项目被称为Root Project,而每个模块其实都是一个子project。Root Project肯定是要参与编译的,但Gradle是怎么知道哪些子Project需要参与编译呢?其实它就是在Initialization阶段执行settings.gradle脚本得到的,这个脚本会将子Project给include进来。

  • Configuration (配置)

每个Project都与一个build.gradle文件一一对应,build.gradle可以说就是Project的配置脚本。在配置阶段Gradle会执行所有参与编译的Project的build.gradle脚本,这个脚本对Project进行配置并创建一系列Task,这些Task之间会有依赖关系,构成一个有向无环图。

  • Execution (执行)

执行阶段我们可以选择一个或者多个Task去执行。被执行到的Task可能会依赖其他的Task,Gradle保证这些Task按照依赖关系的顺序执行,并且每个任务只执行一次。

我们可以通过添加打印查看settings.gradle和build.gradle被执行的时机:

1
2
3
4
5
//settings.gradle
include ':app'
rootProject.name='My Application'

println("---> settings.gradle")
1
2
3
4
5
6
7
8
9
10
11
12
13
// 根目录的build.gradle
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
// app下的build.gradle
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
// app build.gradle
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") })
//基于groovy语法我们也可以省略圆括号,写成下面的样子:
//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) {
// Matches: task <name-value-pairs>, <identifier>, <arg>?
// Map to: task(<name-value-pairs>, '<identifier>', <arg>?)
transformVariableExpression(call, 1);
} else if (args.getExpression(0) instanceof VariableExpression) {
// Matches: task <identifier>, <arg>?
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
//上面的代码实际是下面代码的省略形式
//project.TaskC.dependsOn(project.TaskA)
//project.TaskC.dependsOn(project.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
// app build.gradle

// 创建自定义类
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拓展myExt方法
project.extensions.add("myExt", ext)

// 下面的myExt实际是project.myExt省略project对象,函数调用的圆括号也被省略了
myExt {
data1("111")
data2("222")
}

println(myExt.data1)
println(myExt.data2)