Task 执行顺序与依赖调试
Gradle 构建的核心是 Task(任务)及其依赖关系。Task 的执行顺序由依赖图(Task Graph)决定,而依赖图的正确性直接影响构建的正确与效率。本章围绕以下关键点展开:如何声明依赖(dependsOn / finalizedBy)、如何查看 Task 图、如何调试 Task 的输入/输出以及如何利用增量构建的 up-to-date 状态避免重复执行。
4.1 原理:依赖图与执行顺序
Gradle 在配置阶段结束后会构建一个有向无环图(DAG),即 Task Graph。每个 Task 是一个节点,依赖关系(dependsOn、mustRunAfter、shouldRunAfter、finalizedBy)构成边。执行阶段按照拓扑排序的顺序执行 Task,确保每个 Task 的所有前置依赖(dependsOn)先执行。finalizedBy 则用于定义“终态任务”——无论当前 Task 是否成功,都会在之后执行。
dependsOn 强制前置执行,mustRunAfter 只要求顺序但不存在硬依赖,finalizedBy 在当前 Task 完成后执行(即使当前 Task 失败)。
4.2 实现流程:声明依赖与触发顺序
- 使用
dependsOn:在当前 Task 上显式声明前置 Task。 - 使用
finalizedBy:在当前 Task 动作完成后追加执行另一个 Task。 - 使用
mustRunAfter/shouldRunAfter:仅调整顺序,不添加依赖。 - 调用
gradle.taskGraph:在配置阶段或执行阶段前访问图并添加条件逻辑。
一般推荐在 build.gradle.kts 的配置阶段声明依赖,Gradle 自动构造图。
4.3 代码示例:dependsOn 与 finalizedBy
以下 Kotlin DSL 脚本演示三种依赖声明方式:
// build.gradle.kts
tasks.register("clean") {
doLast { println("Cleaning...") }
}
tasks.register("compile") {
dependsOn("clean")
doLast { println("Compiling...") }
}
tasks.register("test") {
dependsOn("compile")
mustRunAfter("compile") // 与 dependsOn 冗余,仅作演示
doLast { println("Testing...") }
}
tasks.register("report") {
finalizedBy("sendEmail") // 无论 report 成功还是失败,都会执行 sendEmail
doLast { println("Generating report...") }
}
tasks.register("sendEmail") {
doLast { println("Sending email...") }
}
执行 gradle test report 时,Task 图的执行顺序为:clean → compile → test(并行阶段编译),随后 report → sendEmail。
4.4 调试 Task Graph:查看图信息
Gradle 提供了多个命令来诊断 Task 图:
gradle tasks --all:列出所有 Task 及其依赖关系。gradle taskGraph info(需插件gradle-taskgraph)或使用内置 API。gradle:模拟执行,显示计划顺序。--dry-run - 在构建脚本中打印图结构:
// build.gradle.kts
gradle.taskGraph.whenReady {
println("Task Graph:")
allTasks.forEach { task ->
println(" ${task.path} dependsOn: ${task.taskDependencies.getDependencies(task)}")
}
}
运行时输出类似:
> Task :clean
> Task :compile
依赖: [:clean]
最终执行: [:sendEmail]
...
--profile 参数可生成 HTML 报告,包含 Task 执行时间与依赖链分析。
4.5 调试 Task 输入输出
Task 的输入输出声明决定了增量构建的 up-to-date 判断。常见调试方式:
- 在 Task 内添加
@get:Input、@get:OutputFile等注解。 - 查看 up-to-date 原因:
gradle会输出类似 “Task ‘:compile’ is not up-to-date because: …” 的信息。--info - 使用
gradle buildEnvironment检查依赖文件散列值。 - 通过
build/reports/up-to-date-report.html(需插件)查看详细比较。
示例:声明输入输出并调试
abstract class GenerateConfig @Inject constructor(
private val objectFactory: ObjectFactory
) : DefaultTask() {
@get:Input
val version: Property<String> = objectFactory.property()
@get:OutputFile
val outputFile: RegularFileProperty = objectFactory.fileProperty()
@TaskAction
fun generate() {
outputFile.get().asFile.writeText("version=${version.get()}")
}
}
tasks.register<GenerateConfig>("generateConfig") {
version.set("2.0")
outputFile.set(layout.buildDirectory.file("config.properties"))
}
运行 gradle generateConfig --info 会输出:
Executing task ':generateConfig' (up-to-date check took 0.012 sec,
reason: Input property 'version' has value '2.0', output file ...
Output file changed, so not up-to-date)
4.6 检查 up-to-date 状态
Gradle 使用输入/输出的散列快照判断增量构建。以下表格总结了常见状态与调试手段:
| 状态 | 含义 | 调试命令/方法 |
|---|---|---|
| UP-TO-DATE | 自上次执行后输入/输出均未变化,跳过 Task | --info 查看原因;--rerun-tasks 强制重新执行 |
| FROM-CACHE | 从构建缓存恢复 | --build-cache 启用;检查 build/tmp 镜像 |
| NO-SOURCE | 没有输入文件,Task 跳过 | 检查 inputs.files 是否为空 |
| EXECUTED | Task 实际运行 | 关注输出中是否出现异常 |
强制禁用 up-to-date 检查进行调试:
// build.gradle.kts
tasks.named<Test>("test") {
outputs.upToDateWhen { false } // 始终视为过时
}
// 或命令行:gradle test --rerun-tasks
outputs.upToDateWhen { false } 会破坏增量构建性能,仅用于临时调试。
4.7 常见错误与优化
- 循环依赖:Gradle 会在配置阶段抛出
Circular dependency异常。排查时使用--dry-run无法检测(配置阶段已崩溃),应检查dependsOn链。 - finalizedBy 导致的意外执行:被 finalize 的 Task 不受
onlyIf或失败影响,需确认是否期望无条件执行。 - 输入/输出声明不完整:导致 Task 永远 UP-TO-DATE 或永远执行。务必对文件/属性使用
@InputFiles、@InputDirectory等注解。 - 性能优化:合理使用
mustRunAfter代替dependsOn可以允许并行执行;使用finalizedBy的后处理任务尽量轻量。
定期运行 gradle --build-cache 并检查 build/reports 下的 profile 报告,可发现执行瓶颈。