组件化Gradle语法

Gradle作为一款优秀的构建工具,也作为是目前Android主流的构建工具,不管是通过命令行还是通过GUI方式构建,背后都是通过Gradle来实现的,所以学习Gradle非常重要。无论是组件化、插件化、热修复等技术都需要对Gradle比较了解,不懂Gradle将无法完成上述事情,所以Gradle必须要学习。这篇文章主要是针对组件化的Android项目,如何使用Gradle完成公共配置项的抽取,Gradle如何定义Task,工程测试环境与正式环境的自动配置等内容。

Gradle = Groovy/Kotlin + DSL

Gradle可以看成Groovy + Gradle DSL + Android DSL。DSL的全称是Domain Specific Language,即领域特定语言,其实就是这个语言不通用,只能用于特定的某个领域。因此DSL也是语言。Groovy是一门JVM语言,和Kotlin一样最终是要编译成class文件然后在JVM上执行,所以Java语言的特性Groovy都支持,我们完全可以混写Java和Groovy。

使用Groovy就像使用JavaScript一样简单,Groovy提供了更加灵活简单的语法,大量的语法糖以及闭包特性可以让你用更少的代码来实现和Java同样的功能。比如解析xml文件,Groovy就非常方便,只需要几行代码就能搞定,而如果用Java则需要几十行代码。关于Groovy的语法,我之前写过一篇博客 《Gradle的使用和配置》 ,简单介绍了Groovy打印字符串,定义变量、方法、集合、闭包、List/Map等。更多详细的语法内容可以参考官方文档: http://www.groovy-lang.org/api.html

Gradle的执行顺序

新建一个Android工程,有settings.gradle、整个工程的build.gradle、app module的build.gradle。

Gradle脚本的执行分为三个过程:

1、初始化:分析有哪些module将要被构建,为每个module创建对应的 project实例。这个时候settings.gradle文件会被解析。

2、配置:处理所有的模块的 build 脚本,处理依赖,属性等。这个时候每个模块的build.gradle文件会被解析并配置,这个时候会构建整个task的链表(这里的链表仅仅指存在依赖关系的task的集合,不是数据结构的链表)。

3、执行:根据task链表来执行某一个特定的task,这个task所依赖的其他task都将会被提前执行。

分别在三个文件里打印一下执行顺序如下:

Gradle的Task

定义Gradle的Task

Task可以理解为Gradle的执行单元,Gradle通过一个个task来完成具体的构建任务,下面我们来学习一下Task的定义:

通过上述方式定义的task,括号内部的代码会在配置阶段执行,也就是说,只要我执行任何一个task,那段代码都会执行,因为每个task执行之前都需要进行一遍完整的配置。但是很多时候我们并不需要写配置代码,我们想要括号内的代码仅仅在执行我们的task的时候才执行,这个时候可以通过doFirst或者doLast来完成。需要注意的是,一个project包含多个Task,一个Task包含多个Action,这里的Action就是完成一个Task需要的具体操作:

这样的话执行aMyTask会出现如下结果:

1......
2
3> Task :aMyTask
4before execute aMyTask2
5before execute aMyTask1
6after execute aMyTask1
7after execute aMyTask2
8
9BUILD SUCCESSFUL in 717ms

注意:println "run aMyTask" 这句代码并不是在队列的中间执行,这句代码是在配置阶段执行的,所有直接在Task里写的动作并不会添加到 Task 的Action列表中,只会当做 Task 的配置信息执行,所以声明周期一定要搞清楚。!

不过通过 extends DefaultTask可以在里面使用@TaskAction注解,这样等同于把注解的Action动作添加到了Action队列里,参考下面Task也可以继承的例子:

Gradle api 还给我们提供了其他的方式创建Task:

1tasks.create("aMyTask3"){
2    println "run aMyTask3 ..."
3}

Task也可以继承:

 1class MyTask4 extends DefaultTask {
 2
 3    @TaskAction
 4    void action(){
 5        println "run MyTask4 ..."
 6    }
 7}
 8
 9//创建 aMyTask5
10task aMyTask5 (type: MyTask4){
11    doLast {
12        println "run MyTask5 ..."
13    }
14}

输出为:

1......
2
3> Task :aMyTask5
4run MyTask4 ...
5run MyTask5 ...
6
7BUILD SUCCESSFUL in 686ms
81 actionable task: 1 executed

Task的属性与方法

Task的常见属性如下:

属性名 描述
actions 该任务将要执行的一系列动作
dependsOn 返回该任务依赖的任务
description 任务的描述
enabled 该任务是否开启
finalizedBy 返回完成此任务之后的任务
group 任务的分组
mustRunAfter 返回该任务必须在哪个任务之后运行的任务
name 任务的名字
path 任务的路径
project 任务所属的 Project

Task的常见方法如下:

方法名(不列出参数) 描述
dependsOn 给任务设置依赖任务
doFirst 给 Task 添加一个任务动作开始执行之前的动作
doLast 给 Task 添加一个任务动作执行结束之后的动作
finalizedBy 给任务添加终结任务,即该任务结束后执行的任务
hasProperty 判断该任务是否有指定属性
mustRunAfter 声明该任务必须在某些任务之后执行
onlyIf 给任务添加断言,只有满足条件才可以执行任务
property 返回指定属性的值
setProperty 修改指定属性的值

Gradle声明周期

其实就是上面说到的Gradle的执行顺序,这里只不过是换个说法而已。

1、初始化阶段

会去读取根工程中setting.gradle中的include信息,决定有哪几个工程加入构建,创建project实例,比如下面有三个工程:include ‘:app’, ‘:lib1’, ':lib2'

2、配置阶段

会去执行所有工程的build.gradle脚本,配置project对象,一个对象由多个任务组成,此阶段也会去创建、配置task及相关信息。

3、运行阶段

根据Gradle命令传递过来的Task名称,执行相关依赖任务,Task的Action会在这个阶段执行。

Task依赖与顺序

一个Project拥有多个Task,这些Task之间的关系由有向无环图维护。而有向无环图是在构建的配置过程中生成的,我们可以通过 gradle.taskGraph 来监听这个过程。

dependsOn 给某个任务设置依赖任务

 1
 2// 定义taskA
 3task taskA {
 4    doLast {
 5        println 'TaskA run ...'
 6    }
 7}
 8
 9// 定义taskB extend taskA
10task taskB {
11    dependsOn taskA // 通过方法设置
12    doLast {
13        println 'TaskB run ...'
14    }
15}
16
17// 定义taskC extend taskA
18task taskC(dependsOn: taskA) {
19    dependsOn taskA // 通过Map参数依赖任务A
20    doLast {
21        println 'TaskC run ...'
22    }
23}
24
25// 定义taskD
26task taskD {
27    doLast {
28        println 'TaskD run ...'
29    }
30}
31
32// 定义taskE extend taskA, taskD
33task taskE {
34    doLast {
35        println 'TaskE run ...'
36    }
37}
38taskE.dependsOn taskA, taskD // 通过dependsOn方法同时依赖两个任务A和D

finalizedBy 给某个任务设置终结任务。

 1// 定义任务A
 2task taskA {
 3    doLast {
 4        println 'TaskA run ...'
 5    }
 6}
 7
 8// 定义任务B
 9task taskB {
10    finalizedBy taskA // 将任务A设置成任务B的终结任务
11    doLast {
12        println 'TaskB run ...'
13    }
14}
15
16// 定义任务C
17task taskC {
18    doLast {
19        println 'TaskC run ...'
20    }
21}
22
23// 定义任务D
24task taskD {
25    doLast {
26        println 'TaskD run ...'
27    }
28}
29
30// 任务D执行后,立刻执行任务A和任务C
31taskD.finalizedBy taskA, taskC

对任务进行 finalizedBy 配置和 dependsOn 很类似,其作用和 dependsOn 恰好相反。在某任务执行完后,会执行其设置的终结任务。

mustRunAfter 如果 taskB.mustRunAfter(taskA) 则表示 taskB 必须在 taskA 执行之后再执行,这个规则比较严格。

 1task taskA {
 2    doLast {
 3        println 'TaskA run ...'
 4    }
 5}
 6
 7task taskB {
 8    doLast {
 9        println 'TaskB run ...'
10    }
11}
12
13// 任务A必须在任务B之后执行
14taskA.mustRunAfter taskB

运行命令 ./gradlew taskA taskB ,就会发现 taskB 会先执行。

如何跳过Task

有时候某些任务需要禁止执行或者满足某个条件才能执行,Gradle 提供了多种方式来跳过任务。

方式一:每个任务都有个 enabled 属性,可以启用和禁用任务,默认是 true,表示启用。如果设置为 false ,则会禁止该任务执行。

1// 使用./gradlew disableTask运行
2task disableTask {
3    enabled false // 1、直接方法设置
4    doLast {
5        println 'disableTask run ...'
6    }
7}
8disableTask.enabled = false // 2、直接属性设置

方式二:使用 onlyIf 判断方法,只有当 onlyIf 里返回 true 时该任务才可以执行。

 1// 使用gradlew sayBye -Pxx运行,这里的-P是添加参数的意思
 2task onlyIfTestTask {
 3    doLast {
 4        println 'onlyIfTestTask run ...'
 5    }
 6}
 7
 8// 只有当project中没有xx属性时,任务才可以执行
 9onlyIfTestTask.onlyIf {
10    !project.hasProperty('xx') 
11}

方式三:使用 StopExecutionException 。如果任务抛出这个异常,Gradle 会跳过该任务的执行,转而去执行下一个任务。

 1// 使用./gradlew taskA运行
 2task taskA {
 3    doLast {
 4        // 不会影响后续任务的执行
 5        throw new StopExecutionException()
 6    }
 7}
 8
 9task taskB(dependsOn: taskA) {
10    doLast { // 并不影响nextTask的执行
11        println 'taskB run ...'
12    }
13}

方式四:利用 Task 的 timeout 属性来限制任务的执行时间。一旦任务超时,它的执行就会被中断,任务将被标记失败。Gradle 中内置任务都能及时响应超时。

 1// 故意超时
 2task taskA {
 3    doLast {
 4        Thread.sleep(100000)
 5    }
 6    timeout = Duration.ofMillis(500)
 7}
 8
 9task taskB(dependsOn: taskA) {
10    doLast { // 并不影响nextTask的执行
11        println 'taskB run ...'
12    }
13}

其实常用的还是方法一和方法二。

上面已经说完了如何创建和使用 Task,Task作为Gradle的主要执行骨架是非常重要的,我们可以通过 Task 的各种属性、方法来灵活地配置和调整任务的依赖、执行顺序以及运行规则。

抽取Gradle中的重复项

回到组件化配置相关的内容,此时再新建一个名为mylibrary的Android Library module,mylibrary的build.gradle和app module的build.gradle有很多重复项,那么如何抽取出这部分的重复项目呢?

首先在项目根目录下新建一个app_config.gradle,在里面抽取出公共的配置:

 1// 把公用的配置项提取出来
 2// 整个App项目的配置文件
 3
 4// ext 自定义我们的内容
 5ext {
 6    username = "changlin"
 7
 8    // 抽取出公共项,定义key-value map
 9    app_android = [
10            compileSdkVersion : 30,
11            buildToolsVersion  : "30.0.3",
12            minSdkVersion : 23,
13            targetSdkVersion : 30,
14            versionCode : 1,
15            versionName : "1.0",
16            testInstrumentationRunner : "androidx.test.runner.AndroidJUnitRunner"
17    ]
18
19    // 依赖相关的内容
20    app_impl = [
21            "appcompat": 'androidx.appcompat:appcompat:1.2.0',
22            "material": 'com.google.android.material:material:1.3.0',
23            "junit": 'junit:junit:4.13.2',
24            "androidx_junit": 'androidx.test.ext:junit:1.1.2',
25            "androidx_espresso": 'androidx.test.espresso:espresso-core:3.3.0'
26    ]
27
28    // 编译相关的内容
29    app_compile = [
30            sourceCompatibility: JavaVersion.VERSION_1_8,
31            targetCompatibility: JavaVersion.VERSION_1_8,
32    ]
33}

在整个项目的build.gradle中引入app_config.gradle:

 1println 'build.gradle run ...'
 2
 3// 加载项目的gradle的时候,就引入app_config.gradle
 4apply from : 'app_config.gradle'
 5
 6buildscript {
 7    ...
 8}
 9
10allprojects {
11    ...
12}

接下来在app module与mylibrary module中使用定义好的配置即可,app module的build.gradle:

 1// app module的build.gradle
 2plugins {
 3    id 'com.android.application'
 4}
 5
 6println 'app -> build.gradle run ...'
 7
 8// 使用app_config.gradle, rootProject是内置的对象
 9def my_name = this.rootProject.ext.username
10println my_name
11
12println "rootProject.name = ${rootProject.name}"
13
14android {
15    compileSdkVersion app_android.compileSdkVersion
16    buildToolsVersion app_android.buildToolsVersion
17
18    defaultConfig {
19        applicationId "com.tal.learn_gradle"
20        minSdkVersion app_android.minSdkVersion
21        targetSdkVersion app_android.targetSdkVersion
22        versionCode app_android.versionCode
23        versionName app_android.versionName
24
25        testInstrumentationRunner app_android.testInstrumentationRunner
26    }
27
28    buildTypes {
29        release {
30            minifyEnabled false
31            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
32        }
33    }
34    compileOptions {
35        sourceCompatibility app_compile.sourceCompatibility
36        targetCompatibility app_compile.targetCompatibility
37    }
38}
39
40dependencies {
41    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
42
43//    implementation app_impl.appcompat
44//    implementation app_impl.material
45//    testImplementation app_impl.junit
46//    androidTestImplementation app_impl.androidx_junit
47//    androidTestImplementation app_impl.androidx_espresso
48
49
50    // 更简化的方式,但是这样都是以implementation的方式添加进来的
51    app_impl.each {
52        k, v ->
53            implementation v
54            println "引入 > ${k}"
55    }
56
57}

mylibrary module的build.gradle:

 1plugins {
 2    id 'com.android.library'
 3}
 4
 5def app_android = this.rootProject.ext.app_android
 6
 7android {
 8    compileSdkVersion app_android.compileSdkVersion
 9    buildToolsVersion app_android.buildToolsVersion
10
11    defaultConfig {
12        minSdkVersion app_android.minSdkVersion
13        targetSdkVersion app_android.targetSdkVersion
14        versionCode app_android.versionCode
15        versionName app_android.versionName
16
17        testInstrumentationRunner app_android.testInstrumentationRunner
18        consumerProguardFiles "consumer-rules.pro"
19    }
20
21    buildTypes {
22        release {
23            minifyEnabled false
24            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
25        }
26    }
27
28    compileOptions {
29        sourceCompatibility app_compile.sourceCompatibility
30        targetCompatibility app_compile.targetCompatibility
31    }
32}
33
34dependencies {
35    implementation app_impl.appcompat
36    implementation app_impl.material
37    testImplementation app_impl.junit
38    androidTestImplementation app_impl.androidx_junit
39    androidTestImplementation app_impl.androidx_espresso
40}

提取这么多重复项目,来看看结果吧,依旧是成功编译:

环境参数自动变更

比如在app_config.gradle里面定义两个URL:

1// debug/release模式下的server_url
2app_server_url = [
3  "debug": "http://test.xxx.com/xxx",
4  "release": "http://product.xxx.com/xxx"
5]

现在只需要在app module的build.gradle里面如下写法:

 1buildTypes {
 2  debug {
 3    // 测试环境用debug_url
 4    buildConfigField("String", "SERVER_URL", "\"${app_server_url.debug}\"")
 5  }
 6  release {
 7    // 正式环境用release_url
 8    buildConfigField("String", "SERVER_URL", "\"${app_server_url.release}\"")
 9    ...
10  }
11}

Build一下工程,便会在BuildConfig中自动生成URL常量:

另外,如果想在代码中使用自己在app_config.gradle定义的isRelease字段可以这样使用:

 1// app -> build.gradle
 2android {
 3    compileSdkVersion app_android.compileSdkVersion
 4    buildToolsVersion app_android.buildToolsVersion
 5
 6    defaultConfig {
 7        ...
 8        targetSdkVersion 30
 9        versionCode 1
10        versionName "1.0"
11				...
12        //这个方法接收三个非空的参数,第一个:确定值的类型;第二个:指定key的名字;第三个:传值(必须是String
13        //为什么需要定义这个?因为src代码中有可能需要用到跨模块交互,如果是组件化模块显然不行
14        //切记:不能在android根节点,只能在defaultConfig或buildTypes节点下
15        buildConfigField("boolean", "isRelease", String.valueOf(isRelease));
16    }
17  
18    ...
19}

参考资料

Gradle官方使用手册 https://docs.gradle.org/current/userguide/userguide.html

Groovy语言官方API http://www.groovy-lang.org/api.html