4. Gradle Task详解:Task的创建方式、Task的配置与执行、Task依赖与排序、Task的输入输出、增量构建原理
说实话,Task是Gradle构建脚本里最核心的概念。你写的每一行构建逻辑,最终都会落到某个Task上。我刚开始接触Gradle时,总觉得Task就是个「跑脚本」的东西,后来踩了不少坑才明白——Task的设计远比我想象的讲究。
这一章,咱们就把Task彻底聊透。从怎么创建,到怎么配置,再到怎么让它们有序执行,最后聊聊Gradle引以为傲的增量构建。嗯,内容不少,但都是干货。
4.1 Task的创建方式
Task的创建方式有好几种,我个人最常用的是通过tasks.register()。为什么?因为它支持延迟创建,只有Task真正需要执行时才会被实例化。这在大型项目里能明显提升配置阶段的性能。
// 方式一:使用 register(推荐)
tasks.register('hello') {
doLast {
println 'Hello, Gradle!'
}
}
// 方式二:使用 create(立即创建)
tasks.create('helloOld') {
doLast {
println 'Hello, old way!'
}
}
// 方式三:通过已有的 Task 类型
tasks.register('myCopy', Copy) {
from 'src'
into 'dest'
}
关键区别:register()是延迟创建,create()是立即创建。在配置阶段,register()不会实例化Task对象,只有执行阶段才会。我建议你养成用register()的习惯。
你可能会问:「那我什么时候用create()?」说实话,我只有在写快速验证脚本时才会用create()。生产代码里,一律register()。
4.2 Task的配置与执行
Task的生命周期分为两个阶段:配置阶段和执行阶段。这个区分特别重要,我见过不少同事把逻辑写错地方,导致每次构建都跑一遍不该跑的代码。
tasks.register('example') {
// 配置阶段执行 —— 每次构建都会执行
println '我在配置阶段执行'
doFirst {
// 执行阶段执行 —— 只有Task被调用时才执行
println '我在执行阶段,最先执行'
}
doLast {
// 执行阶段执行 —— 只有Task被调用时才执行
println '我在执行阶段,最后执行'
}
}
我曾经犯过一个错误:在配置阶段写了一个耗时的网络请求,结果每次执行gradle tasks都要等好几秒。后来才意识到,应该把耗时操作放到doLast里。
说白了,配置阶段的代码每次构建都会跑,不管你这个Task有没有被执行。执行阶段的代码,只有Task被真正执行时才会跑。这个区别,你品,你细品。
4.3 Task依赖与排序
Task之间经常有依赖关系。比如打APK之前,必须先编译代码。Gradle提供了几种方式来控制Task的执行顺序。
| 方式 | 说明 | 示例 |
|---|---|---|
dependsOn |
强依赖,先执行依赖的Task | taskB.dependsOn(taskA) |
mustRunAfter |
排序约束,不强制依赖 | taskB.mustRunAfter(taskA) |
shouldRunAfter |
弱排序,可被忽略 | taskB.shouldRunAfter(taskA) |
finalizedBy |
无论成功失败,最后执行 | taskA.finalizedBy(taskB) |
tasks.register('compile') {
doLast { println '编译中...' }
}
tasks.register('test') {
dependsOn 'compile'
doLast { println '测试中...' }
}
tasks.register('clean') {
doLast { println '清理中...' }
}
tasks.register('build') {
dependsOn 'test'
finalizedBy 'clean'
doLast { println '构建完成' }
}
这里有个细节:dependsOn和mustRunAfter的区别。前者是「我依赖你,你必须先执行」,后者是「如果你也要执行,那你必须在我前面」。你想想看,如果taskA和taskB没有依赖关系,但你想让它们按顺序执行,就用mustRunAfter。
4.4 Task的输入输出
Task的输入输出是Gradle增量构建的基础。说白了,Gradle通过比较输入输出的状态,来判断一个Task是否需要重新执行。
abstract class MyTransformTask extends DefaultTask {
@InputFile
abstract RegularFileProperty getInputFile()
@OutputFile
abstract RegularFileProperty getOutputFile()
@TaskAction
void transform() {
inputFile.get().asFile.copyTo(outputFile.get().asFile)
println "文件已转换: ${inputFile.get()} -> ${outputFile.get()}"
}
}
tasks.register('transformFile', MyTransformTask) {
inputFile.set(file('input.txt'))
outputFile.set(file('output.txt'))
}
核心注解:
@InputFile/@InputFiles:标记输入文件@OutputFile/@OutputDirectory:标记输出文件@Input:标记普通输入参数@TaskAction:标记Task执行的方法
我记得有一次,项目构建越来越慢,排查后发现有个Task没有声明输入输出。每次构建都重新执行,哪怕输入文件根本没变。加上注解后,构建时间从40秒降到了5秒。嗯,增量构建的威力就在这里。
4.5 增量构建原理
增量构建,说白了就是「只做必要的,跳过不必要的」。Gradle通过比较Task的输入输出状态,来决定是否跳过执行。
具体原理是这样的:
- Gradle会记录每个Task的输入输出「快照」
- 下次构建时,重新计算输入输出的快照
- 如果快照没变,Gradle就认为Task是UP-TO-DATE,直接跳过
- 如果快照变了,Task才会重新执行
小技巧:执行gradle build --info可以看到哪些Task被标记为UP-TO-DATE。如果某个Task明明输入没变却每次都执行,说明你的输入输出声明有问题。
下面这张图展示了增量构建的核心流程:
你可能会问:「Gradle怎么知道输入输出变了?」答案是快照对比。Gradle会计算输入文件的哈希值、文件大小、最后修改时间等信息,生成一个快照。下次构建时,重新计算快照,一对比就知道变没变。
注意:如果你的Task没有声明任何输入输出,Gradle会认为它永远需要执行。这就是为什么有些Task每次构建都跑,明明什么都没变。我曾经接手过一个项目,里面有个Task没加任何注解,每次构建都跑30秒,加上注解后直接变成0秒。
最后说一个实用技巧:如果你想强制某个Task重新执行,可以用--rerun-tasks参数。比如gradle build --rerun-tasks。不过一般情况下别用,除非你在调试。
好了,Task的核心内容就这些。从创建到配置,从依赖到增量构建,每个点都值得你在实际项目中反复体会。记住一句话:好的Task设计,能让你的构建又快又稳。