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 失败)。
Task Graph 示例(含 finalizedBy) Task A Task B Task C Task D Task E 图例说明 实线箭头:dependsOn 虚线箭头:finalizedBy 弧线:mustRunAfter 拓扑顺序:A→B→C→(D若C失败)。

4.2 实现流程:声明依赖与触发顺序

  1. 使用 dependsOn:在当前 Task 上显式声明前置 Task。
  2. 使用 finalizedBy:在当前 Task 动作完成后追加执行另一个 Task。
  3. 使用 mustRunAfter / shouldRunAfter:仅调整顺序,不添加依赖。
  4. 调用 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 判断。常见调试方式:

  1. 在 Task 内添加 @get:Input、@get:OutputFile 等注解。
  2. 查看 up-to-date 原因:gradle --info 会输出类似 “Task ‘:compile’ is not up-to-date because: …” 的信息。
  3. 使用 gradle buildEnvironment 检查依赖文件散列值。
  4. 通过 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 是否为空
EXECUTEDTask 实际运行关注输出中是否出现异常

强制禁用 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 报告,可发现执行瓶颈。