任务(Task)详解:创建、依赖、排序、增量与类型
说实话,刚开始用 Gradle 的时候,我最头疼的就是任务这一块。
明明照着文档写了个 task,跑起来却跟预期差得老远。
今天我就把这几个核心点给你掰开揉碎了讲清楚。
1. 任务的创建方式
Gradle 创建任务有几种常见写法。我最早接触的是这种:
task hello {
doLast {
println "Hello, Gradle!"
}
}
后来发现 Groovy DSL 里还能用 tasks.register,这是延迟创建的写法——任务只有被用到时才真正创建。
tasks.register("bye") {
doLast {
println "Goodbye!"
}
}
为什么呢?因为大型项目里任务太多了,全部提前创建会拖慢配置阶段。
我个人习惯:能用 register 尽量用 register,配置阶段能省则省。
| 创建方式 | 特点 | 推荐场景 |
|---|---|---|
| task 关键字 + 闭包 | 立即创建,简单直观 | 小型项目或测试脚本 |
| tasks.register() | 延迟创建,避免无用任务加载 | 多模块、大型项目 |
| tasks.create() | 立即创建,但已废弃 | 老代码里常见,不建议新用 |
我的经验:有一次在 50+ 模块的项目里,把所有 task 改成 register 后,配置阶段从 12 秒降到了 3 秒。差别非常明显。
2. 任务依赖
任务之间经常有先后关系。比如你要先编译,再测试,再打包。
Gradle 提供了 dependsOn 来声明依赖。
task compile {
doLast { println "编译完成" }
}
task test {
dependsOn compile
doLast { println "测试完成" }
}
task package {
dependsOn test
doLast { println "打包完成" }
}
跑 gradle package 的时候,Gradle 会自动把 compile 和 test 都跑一遍。
但注意:dependsOn 不会强制排序。如果你有两个并行任务都依赖同一个任务,它们会一起等待,但相互之间谁先谁后不一定。
这时就需要用到下面的排序机制。
我曾经掉过的坑:在某个 CI 脚本里,用 dependsOn 声明了 A → B → C,结果打出的包总是缺少 B 生成的资源。排查半天才发现是 B 本身没有直接依赖 A,而是通过另一个任务间接依赖的。最后老老实实把整条链显式写出来了。
3. 任务排序
排序和依赖是两回事。依赖讲究「必须执行」,排序讲究「执行顺序」。
Gradle 提供了两个让你手动干预的方法:
taskA.mustRunAfter taskB—— 如果 A 和 B 同时存在,A 必须在 B 之后跑taskA.shouldRunAfter taskB—— 建议在之后,但优先级不是强制的
举个例子:
task lint { doLast { println "代码检查" } }
task checkStyle { doLast { println "风格检查" } }
checkStyle.mustRunAfter lint
如果你同时跑 gradle lint checkStyle,Gradle 会先跑 lint,再跑 checkStyle。
如果只跑其中一个,那排序声明不会生效。
重点:mustRunAfter 不会自动触发依赖关系。它只控制顺序,不强制执行。
4. 增量构建
这个特性我特别喜欢——它让二次构建快得像闪电。
核心思想:输入没变,输出就不重新生成。
Gradle 通过 @Input 和 @Output 注解来识别输入输出。
abstract class MyTask extends DefaultTask {
@Input
abstract Property<String> getSourceDir()
@OutputDirectory
abstract DirectoryProperty getOutputDir()
@TaskAction
void doWork() {
// 实际的业务逻辑
}
}
只要 sourceDir 里的内容没变,outputDir 也存在,Gradle 就会跳过这个任务。
而且它会自动对比文件的 lastModified 和 checksum。
| 注解 | 作用 | 示例 |
|---|---|---|
| @Input | 标识输入文件/属性 | @InputFile, @InputDirectory |
| @Output | 标识输出文件/目录 | @OutputFile, @OutputDirectory |
| @InputFiles | 多个输入文件 | fileTree |
| @OutputFiles | 多个输出文件 | fileCollection |
注意:如果你忘了声明 @Output,Gradle 就会认为每次都要执行。我之前接手一个遗留项目,所有自定义任务都没标注输出,每次构建都全量跑,浪费了十几分钟。加上 @Output 后,情况一下就改善了。
5. 任务类型
Gradle 内置了很多任务类型,比如 Copy、Exec、Zip、Delete 等。
直接用这些类型,能省掉很多样板代码。
比如复制文件:
tasks.register("copyFiles", Copy) {
from "src/main/resources"
into "build/resources"
include "**/*.properties"
}
这段代码比手动写 doLast { ... } 干净多了。
而且这些内置类型都自带增量构建支持——因为它们已经标注好了 @Input 和 @Output。
自定义任务类型建议继承 DefaultTask,然后用 @TaskAction 标记执行的方法:
abstract class GenerateReport extends DefaultTask {
@Input
abstract Property<String> getReportName()
@OutputFile
abstract RegularFileProperty getOutputFile()
@TaskAction
void generate() {
outputFile.get().asFile.write("Report: ${reportName.get()}")
}
}
tasks.register("genReport", GenerateReport) {
reportName = "Weekly"
outputFile = layout.buildDirectory.file("report.txt")
}
我的习惯:能用内置类型(Copy、Zip)就不用自定义。实在需要自定义,一定把 @Input 和 @Output 写全。这样增量构建才能生效。
上面这张图把任务相关的五个核心概念串起来了。
创建、依赖、排序、增量、类型——它们之间不是孤立的。
比如你用了 Copy 类型(内置类型),自动就获得了增量支持。
又比如你加了 dependsOn,Gradle 内部会按拓扑排序自动安排执行顺序。
一句话总结
- 创建:尽量用 register,别浪费配置时间
- 依赖:显式声明,别偷懒
- 排序:只在并行执行时用,不替代依赖
- 增量:标注 @Input/@Output,能获得 10 倍加速
- 类型:用内置的,少自己写
我自己记这些概念就一个口诀:「创建靠注册,依赖写清楚,排序不滥用,增量要标注,类型选内置」。
你记住这个,日常工作基本够用了。
嗯,任务这块内容确实不少,但掌握了这些,你就能写好和维护好 Gradle 脚本了。
有什么疑问随时翻翻官方文档,或者跑个 gradle help --task 任务名 查看详情。