本篇主要学习 Gradle 构建脚本的三个基本组建成:
project
项目task
任务property
属性
我将会重点介绍 Task,因为它是所有基础中的基础, project 的知识更多和实战有关(例如模块化),而 property 本身不过就是一个属性变量,没有什么东西可讲。
目录
- 1. Project
- 2. Task
- 2.1 任务的结果
- 2.2 任务的定义
- 2.2.1 任务的创建
- 2.2.2 doFirst 和 doLast 的定义
- 2.2.3 调用任务
- 2.2.4 任务配置
- 2.2.5 参数传递
- 2.2.6 添加依赖项
- 2.2.7 跳过任务
- 2.2.7.1 使用 `onlyIf`
- 2.2.7.2 使用 `StopExecutionException`
- 2.2.7.3 启动和禁止任务
- 2.3 增量构建
- 2.4 生命周期任务(Lifecycle tasks)
- 3. Property
- 参考
1. Project
在 Gradle 中,一个 Project
表示一个项目,每一个 .gradle
文件都会和一个 Project 对象相关联,它最终会编译成 Project 字节码。 它用于表示一个整体的目标,例如你用一个 Project
来做 Android 的自动化打包、做一个 Web 应用等等。
在运行构建时,Gradle 会实例化一个 Project
,你可以在脚本中通过 project
句柄获取该实例,我们可以调用该实例的方法来获取或配置项目的参数。它是我们构建脚本中与 Gradle 交互的主要 Api。
Project
是一个Java接口,位于:org.gradle.api.Project
,它的成员函数如下图所示:
通过上面这些函数,我们能知道 Project 有这些定义好的能力:
- 可以在项目上创建新任务(
task
函数) - 添加依赖和配置 (
dependencies
和configurations
函数) - 应用插件或其它脚本 (
apply
函数) - 可以通过
getter / setter
来获取属性,例如 name、 description 等等。
我们可以通过 project 句柄来使用这些能力,如:
project.description = "rikka the project"
println("project的描述为:${project.description}")
由于构建脚本是基于 Project
,咱们写的代码都是在 Project 的作用域中,了解 Kotlin DSL 的同学会知道,.kts
文件其实就是在一个 lambda 表达式里面编程,而这个 lambda 表达式的接收者就是 Project
,带有接收者的函数类型的特点是我们可以隐式调用接收者的函数,因此我们不用显示声明 project
:
description = "rikka the project"
println("project的描述为:${description}")
在我们实际的项目中,一般都是多 project 构建,例如 Android 中就有一个 根Project 的 build.gralde 和至少一个 module 的 buidle.gradle。因为项目越复杂,单个 project 承载的任务会越多越臃肿,我们就越有拆分拆模块的必要。这也引申出 模块化 这是个非常重要的概念, Gradle 对于这方面的支持也已经非常强大,会在后面学习到。
2. Task
Task
是 Project
作用域下的执行单元,和 Project 一样,在 Gradle 中,它被赋予了一些能力。
Task
是一个 Java 接口,位于 org.gradle.api.Task
,它的成员函数如下图所示:
通过这些函数,我们能知道 Task 的能力:
- 可以依赖其他 Task(
dependsOn
函数 ) - 可以定义执行阶段初始和末尾动作 (
doFirst
和doLast
函数) - 通过
getter / setter
获取一些配置属性
2.1 任务的结果
之前的章节里,在执行任务时,我们会看到:
请注意最后一行的信息,在平日的 Android 开发中,你会留意到:在点击 build 运行项目时,控制台中会打印 Gradle 的执行信息,可能会有一些 "Task :xxxxx NO-SOURCE"
、"Task :xxxx UP-TO-DATE"
等字样信息, 这些信息其实是 Gradle 任务执行的结果,它有好几种类型,表明任务执行情况,下面介绍这些类型和意义:
- (没有标签) 或
EXCUTED
- 任务已经执行动作
- 任务没有动作,但依赖n个任务,这n个任务都被执行完成
UP-TO-DATE
表示这个任务的结果已经是最新的。如果一个任务只定义了输出, 如果输出不变的话, 它就会被视为 up-to-date- 任务第一次被执行时产生了一个结果,这次执行后会将结果和上次进行比对,发现是一样的,说明这个任务其实没有进行任何改动(这里一般被用做增量构建)
- 任务告诉 Gradle 没有更改其输出
- 任务没有任何动作,有一些依赖,但是这些依赖的结果都是 “
UP-TO-DATE
”、“SKIPPED
”、“FROM-CACHE
” - 任务没有任何动作,也没有任何依赖
FROM-CACHE
任务的输出从原来的输出缓存中还原。SKIPPED
任务没有被执行- 该任务已经在命令行中被明确排除
- 任务有个
onlyIf
谓词,但是该谓词返回 false,所以不执行
NO-SOURCE
任务不需要执行其动作- 任务有定义输入和输出,但是没有输入。例如在使用
javac
命令编译 Java 时,没有输入的.java
文件。
- 任务有定义输入和输出,但是没有输入。例如在使用
上面 UP-TO-DATE
和 FROM-CACHE
看起来有点像,下面引用 What is Different? 来看下它们的区别:
Gradle有两个主要的特性:增量构建、任务输出缓存
- 增量构建:
用于执行那些自上次运行依赖没有发生更改的任务。为了实现这一点, gradle 会将执行的输入输出以“快照”的形式存储在本地目录.gradle
文件中。当你运行一个已经执行且没有更改的任务时,这个任务会打印 “UP-TO-DATE
” - 任务输出缓存
主要用于 CI 环境,当在 CI 运行的每个阶段时,会删除和检出本地目录,这意味着保存缓存的.gradle
本地文件会在每个阶段消失,因此,任务每次被重新构建,增量构建都不会起作用,此时代替它的将是 任务输出缓存,它的能力是在 gradle 执行时,生成一个缓存,应用于填充本地.gradle
目录,当构建缓存被用来重新填充本地目录时,任务被标记为 “FROM-CACHE
”,一旦本地目录被重新填充,下一次执行将把任务标记为“UP-TO-DATE
”而不是“FROM-CACHE
”。
2.2 任务的定义
2.2.1 任务的创建
我们可以通过 task
或 tasks
句柄来创建任务,如下代码所示:
task("HelloWorld") {
println("Hello World")
}
// 这里的用法等于 task("printRikka") {}
tasks.create("printRikka") {
println("Rikka")
}
在上面代码中,我们使用了 task(taskName) { .. }
创建了一个任务。在每次 Gradle 开启时,会先跑一遍它的配置代码。
这里可能会有些小问题:比如我不想它那么早的运行配置代码,如果我弄了很多 task,一些还是非必要的、动态的,那岂不是容易影响性能?
因此 Gradle 提供了懒加载 task,我们可以通过 tasks.register() { .. }
来创建一个懒任务,它在被执行时,才会去配置运行:
// 定义一个懒任务
tasks.register("printHello") {
println("Hello")
}
// Kotlin 可以使用委托进行定义任务
val hello by tasks.registering {
doLast {
print("Hello Delegate last")
}
}
运行 Gradle,你会发现啥都没有打印,只有执行这些任务才会打印, Gradle 推荐我们使用 tasks.register
来创建任务以提高性能,之后的代码我也会这样去配置任务信息。
2.2.2 doFirst 和 doLast 的定义
doFirst
和 doLast
的作用是什么呢?
Task 可以分为两个部分:
- 配置部分
Gradle 会在配置阶段去跑一遍 Task, 例如设置描述、组别 - 执行部分
Task 有实际的功能,例如签名、删除文件等,可能会被其它任务依赖、调用才会去执行,因此不需要在配置阶段就去执行这些任务。所以doFirst
和doLast
的意义就是在执行阶段进行的代码,它们不会在配置阶段执行。
看下面代码:
task("myTask") {
doFirst {
println("first")
}
doLast {
println("last")
}
description = "rikka the project"
println("project的描述为:${description}")
}
若不执行这个 task,而是跑一个简单的 gradle
命令,打印如下:
> Configure project :
project的描述为:rikka the project
而若执行了 gradle myTask
,将会打印:
> Configure project :
project的描述为:rikka the project
> Task :myTask
first
last
doFirst 和 doLast 的特性如下:
- 通过
doFirst / doLast
来编写具体执功能的代码 doFirst / doLast
可以指定多个。- 外部可以指定一个 task 的
doFirst / doLast
,而且这些块会比内部指定的先执行 - 通过
doFirst / doLast
, 我们能对原有 task 进行扩展而不破坏原有代码结构,如:
task("printRikka") {
println("Rikka")
}
// 对已定义的任务增加额外功能
// 这里使用 tasks 来索引全局任务, 也可以使用 tasks.getByName("printRikka") 获取该任务
tasks["printRikka"].doFirst {
println("Hello World")
}
2.2.3 调用任务
当我们创建了一个任务后,后面需要使用这个任务时,该如何调用它呢?
task("printHello") // 创建一个任务
task<Copy>("copyTask") // 创建一个复制文件的任务
// 使用 Kotlin 代理来访问
val printHello by tasks.getting // 指定任务名
println(printHello.name)
val copyTask by tasks.getting(Copy::class) // 指定任务名和类型
println(copyTask.destinationDir)
也可以通过 tasks
来获取:
tasks.register("printHello")
tasks.register<Copy>("copyTask")
println(tasks["printHello"].name)
println(tasks.named("printHello").get().name)
println(tasks.getByName<Copy>("copyTask").destinationDir)
println(tasks.named<Copy>("copyTask").get().destinationDir)
如果这个任务被定义在别的项目中,我们需要使用相对路径 / 绝对路径的方式来访问:
project(":projectA") {
tasks.register("printHello") // 别的项目有一个任务
}
tasks.register("printHello") // 本项目中有一个同名任务
println(tasks.getByPath("printHello").path) // 打印 :printHello
println(tasks.getByPath(":printHello").path) // 打印 :printHello
println(tasks.getByPath("projectA:printHello").path) // 打印 :projectA:printHello
println(tasks.getByPath(":projectA:printHello").path) // 打印 :projectA:printHello
2.2.4 任务配置
Gradle 内置了一些已有的任务类型,方便我们复用。例如复制文件的任务 Copy
,我们通过配置复制的目标文件和目的地,就能轻松创建一个复制任务:
val myCopy = tasks.named<Copy>("myCopy") // 创建一个 Copy 任务,名称为 myCopy
myCopy { // 通过 DSL 进行配置
from("resources") // 使用 api 来配置数据源
into("target") // 使用 api 配置目的地
include("**/*.txt", "**/*.xml", "**/*.properties") // 配置 include 信息
}
这种写法比较冗余,因为它的创建代码和执行代码分离了,不太符合我们的习惯,下面这种写法更加易读:
val myCopy by tasks.existing(Copy::class) { // 创建和执行写在一起, 也可以用 named、getByName
from("resources")
into("target")
}
myCopy { include(...) } // 外部配置
// 也可以这样写:
tasks.register<Copy>("myCopy ") {
from("resources")
into("target")
include(...)
}
2.2.5 参数传递
与有参数可以配置的Task不同,一些任务是需要入参来运行的,之前创建任务的方式不能满足这种条件。为了达到这个目的,我们必须使用 @javax.inject.Inject
注解来创建可入参的任务类:
// 创建一种 Task 类型,它可以接收一个 String 和 Int 作为参数
open class CustomTask @Inject constructor(
private val message: String,
private val number: Int
) : DefaultTask()
接下来,就像使用 Copy 类型任务一样,去使用这个 CustomTask
:
tasks.register<CustomTask>("customTask", "printHello", 11)
// 下面这种方式会麻烦一点:
task("customTask", "type" to CustomTask::class.java, "constructorArgs" to listOf("printHello", 11))
2.2.6 添加依赖项
我们在前面已经提到过任务的依赖了,通过 dependsOn()
即可配置依赖任务,跨项目的任务依赖,也可以通过路径名的方式来添加:
project("projectA") {
tasks.register("taskA") {
dependsOn(":projectB:taskB")
doLast {
println("taskA")
}
}
}
project("projectB") {
tasks.register("taskB") {
doLast {
println("taskB")
}
}
}
// 执行 taskA,将会打印:
taskB
taskA
除了通过任务名称进行依赖,也可以直接使用 Task
对象本身:
val taskA by tasks.registering {
doLast {
println("taskA")
}
}
val taskB by tasks.registering {
doLast {
println("taskB")
}
}
taskA {
dependsOn(taskB)
}
// 执行 taskA, 打印:
taskB
taskA
假如任务需要依赖多个其它任务,一次性写多个 dependsOn
可能会让代码有些臃肿,我们可以使用 Gradle Provider
提供的特性来优化代码:
val taskA by tasks.registering {
doLast {
println("taskA")
}
}
// 使用 Provider 来拿到需要依赖的任务, 这里使用 filter 过滤器
taskA {
dependsOn(provider {
tasks.filter { task -> task.name.startsWith("lib") }
})
}
tasks.register("lib1") {
doLast {
println("lib1")
}
}
tasks.register("lib2") {
doLast {
println("lib2")
}
}
tasks.register("notALib") {
doLast {
println("notALib")
}
}
// 执行 gradle taskA, 打印:
lib1,
lib2,
taskA
2.2.7 跳过任务
Gradle 提供了一些方法来跳过任务,它有三种通用方法,下面来介绍它们。
2.2.7.1 使用 onlyIf
通过配置任务的 onlyIf { .. }
,若 lambda 内部返回 true,该任务才会被执行:
val hello by tasks.registering {
doLast {
println("hello world")
}
}
hello {
onlyIf { !project.hasProperty("skipHello")}
}
// 执行 gradle hello -PskipHello 输出:
Task :hello SKIPPED
2.2.7.2 使用 StopExecutionException
任务通过在执行过程中主动调用该异常,并不会终止整个 Gradle 构建,而将跳过该动作的执行,然后 Gradle 将继续执行下个任务:
val compile by tasks.registering {
doLast {
println("doing compile")
}
}
compile {
doFirst {
// 这里可能会遇到一些问题,不能继续执行该任务了
if (true) {
throw StopExecutionException()
}
}
}
tasks.register("myTask") {
dependsOn(compile)
doLast {
println("do myTask")
}
}
// 执行 gradle myTask ,输出:
do myTask
2.2.7.3 启动和禁止任务
每个任务都有个 enable
配置,默认为 true。反之将不会执行任何动作,被调用时会被标记为 SKIPPED
:
val disableMe by tasks.registering {
doLast {
println("This should not be printed")
}
}
disableMe {
enabled = false
}
// 执行 gradle disableMe 输出:
Task :disableMe SKIPPED
2.3 增量构建
增量构建是 Gradle 最重要的功能之一。在上面我们有介绍到,如果任务执行的结果是 UP-TO-DATE
,那么说明该任务的输入输出没有更改,所以没有重新构建,从而节省了时间。那么增量构建是如何进行的呢?下面来简单阐述下原理。
正常情况下,任务存在一些输入,和一些输出,以下面编译 Java 为例子,输入是 Java 源文件和一些配置信息(如调试信息),输出是字节码文件:
上图中有三个指向 Task 的箭头,但只有两个箭头被认为是输入,这是因为:输入的特征是能够决定输出:
- 源文件:可以决定生成的字节码,所以它肯定是输入
- 版本号:同上,因为不同的 JDK 版本,源文件会生成不同的字节码
- Fork:是一些不会影响输出内容的参数,例如
memoryMaxiumSize
属性,它只是决定了编译环境的最大可用内存,所以对于 Gradle 来说,它只是一个任务的内部属性
Gradle 会检查当次执行任务的输入,和上次任务的输入(缓存在本地 .gradle
文件中),如果它们是相同的,则 Gradle 会任务该任务已是最新的,将会输出 UP-TO-DATE
,并跳过执行。
具体的增量构建将会在之后的章节中详细讲解。
2.4 生命周期任务(Lifecycle tasks)
生命周期任务是不能自行工作的任务,它们通常没有任何动作,它们将一些东西进行了抽象:
- 工作流程步骤(例如:
check
、build
) - 可构建的事务 (例如:创建一个用于调试的 32位 的可执行文件
debug32Executable
) - 一个父任务,用于组合一系列子任务(例如: 运行所有编译任务的
comileAll
)
插件都定义了一些标准的生命周期任务,例如 build
、assemble
、check
,所有主流语言都有相应的插件,它们都具有相同的生命周期任务。除非你去指定执行这些任务,否则它们的执行将会取决其任务依赖。
3. Property
在 Gradle 中,属性有两种,一种是 Project 和 Task 提供 getter / setter
来获取和配置的属性,这些我们在上面的 Task 中有学到,很多任务会自己定义配置属性,另外一种则是自定义属性。
对于自定义属性,我们可以轻松的使用 val、var
创建一个本作用域使用的属性,也就是局部变量:
// 定义一个变量
val dest = "dest"
// 这里表示一个复制任务,将 source 目标复制到 dest
tasks.register<Copy>("copy") {
from("source")
into(dest)
}
有局部变量,也就有全局变量,在 Gradle 中,全局变量被称为 “额外属性”、“外部属性”,可以通过 extra
定义,也可以使用 Kotlin 的委托:
// 使用委托定义一个外部变量
val myVersion by extra("1.1.0.RELEASE")
val myEmail by extra { "build@rikka.org" }
// 使用 extra 为 project 作用域定义属性
extra["myProp"] = "myValue"
extra.set("myProp1", "myValue1")
tasks.register("printProperties") {
doLast {
println(myVersion)
println(myEmail)
println(project.extra["myProp"])
println(project.extra["myProp1"])
println(extra["myProp2"]) // 没有定义,执行到这里会报错
}
}
调用 priontProerties 任务:
参考
- Gradle user guide 中文版
- Gradle官方文档