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被执行的时机:

//settings.gradle
include ':app'
rootProject.name='My Application'

println("---> settings.gradle")
// 根目录的build.gradle
println("---> build.gradle for Root Project")

buildscript {
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.5.1'
    }
}
...
// app下的build.gradle
println("---> build.gradle for app Project")

apply plugin: 'com.android.application'

android {
    ...
}
...

然后执行.gradlew build查看打印:

./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的完整形态应该是这样的:

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:

// app build.gradle
def ADemoTask = project.task("ADemoTask")

这样创建的Task默认分组是other:

1.jpeg

我们可以在创建的时候给它指定分组:

def ADemoTask = project.task("ADemoTask", group: "build")
2.jpeg

Task是由一个Action列表组成的,Action实际上是一个闭包函数。我们可以用doFirst往列表头插入Action,也能用doLast往列表尾插入Action:

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,最终得到的打印是这样的:

> Task :app:ADemoTask
2
1
3
4

Task DSL定义原理

由于task方法支持直接传入一个闭包函数进行配置,所以我们可以写成下面的方式:

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语法,我们可以将这个闭包移出圆括号:

project.task("ADemoTask") {
    it.doFirst{ println("1") }
    it.doFirst{ println("2") }
    it.doLast{ println("3") }
    it.doLast{ println("4") }
}

为了更加的简洁,我们可以省略project和it对象,然后把task方法函数调用的圆括号也省略:

task "ADemoTask" {
    doFirst{ println("1") }
    doFirst{ println("2") }
    doLast{ println("3") }
    doLast{ println("4") }
}

Gradle在处理build.gradle的时候会有一个转换,将task identifier arg转换成task "identifier" arg(下面的是Gladle的源码从知乎上看到的):

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定义方式:

task ADemoTask {
    println("ADemoTask...")
    doFirst{ println("1") }
    doFirst{ println("2") }
    doLast{ println("3") }
    doLast{ println("4") }
}

所以ADemoTask后方的闭包内的代码(如println)在配置阶段执行,而doFirst/doLast后方的闭包实际是Action,会在执行阶段执行。

Task的依赖关系

如同第一节所说Task之间是会有依赖关系的:


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:

./gradlew TaskC

> Task :app:TaskA
TaskA run...

> Task :app:TaskB
TaskB run...

> Task :app:TaskC
TaskC run...

增量编译

编译一般情况下是通过一些输入的文件来执行编译动作,然后输出另外的文件。在输入文件没有改变的情况下,实际上不需要每次都执行边编译:

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之间的关系大概如下图:

3.png

Project间存在依赖,每个Project包含多个存在依赖的Task,Task内部有一个Action列表,Task的执行实际上就是内部Action的顺序执行。

插件

让我们回到build.gradle脚本,里面默认调用了project的apply、android、dependencies方法:

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都是在这个插件里面定义的。

插件的原理我就不细讲了,刚兴趣的同学可以看这篇笔记,这里只简单举个添加拓展方法的例子:

// 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)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容