任务图与执行顺序

3.1 任务图构建原理

Gradle 在执行任何任务之前,会先将所有声明的任务及其依赖关系构建为一个 有向无环图(DAG,Directed Acyclic Graph)。该图是任务调度的核心数据结构,决定了任务以何种顺序被执行、哪些任务可以并行、哪些必须串行等待。

当 Gradle 解析构建脚本时,每个通过 tasks.register() 或 tasks.create() 声明的任务都会在内部注册为一个 任务节点。节点间通过 dependsOn、mustRunAfter、shouldRunAfter 以及 task.finalizedBy 等声明形成 有向边。构建结束后,整个图必须是严格的 DAG——不允许存在任何环形依赖。

提示: 可以通过 gradle <任务名> --dependency-miss 或 gradle <任务名> --graph 来查看 Gradle 内部构建的任务图结构。在调试复杂多模块调度时,建议使用 --console=plain 观察任务输出顺序。

下面是 Gradle 任务图构建核心流程的示意:

任务图构建三阶段
  1. 解析(Parsing): 读取所有模块的 build.gradle / build.gradle.kts,注册每个任务定义。
  2. 链接(Linking): 遍历 dependsOn、mustRunAfter 等声明,在任务节点之间建立有向边,同时检测环。
  3. 排序(Sorting): 对 DAG 执行拓扑排序,生成最终的线性执行序列;可并行执行的任务会被标记为同一批次。
Gradle 任务图(DAG)构建与拓扑排序示意 :assemble :compileJava :compileKotlin :processResources :test :integrationTest :check mustRunAfter shouldRunAfter 依赖链 = 实线箭头 排序约束 = 虚线箭头 环检测 = 禁止形成回路

3.2 任务排序规则

Gradle 的 TaskSorter 组件对 DAG 执行拓扑排序时,严格遵循以下优先级(从高到低):

优先级 规则类型 说明 是否强制
1 dependsOn 被依赖的任务必须优先执行 强制(违反则报错)
2 mustRunAfter 指定任务必须晚于某任务执行 强制(违反则报错)
3 shouldRunAfter 指定任务尽量晚于某任务执行 非强制(可回退)
4 finalizedBy 指定任务完成后触发另一任务 强制(但追加在尾部)

排序器在处理上述规则时,会首先消除所有 dependsOn 边,然后嵌入 mustRunAfter 边,再尝试嵌入 shouldRunAfter 边。如果 shouldRunAfter 导致环,排序器会 静默丢弃该约束 并回退到无约束顺序。

常见错误:环依赖崩溃
如果 A.dependsOn B 且 B.dependsOn A,或者 A.mustRunAfter B 且 B.mustRunAfter A,Gradle 会在配置阶段抛出 CircularDependencyException,导致构建失败。多模块场景下,跨模块的隐式环更隐蔽,建议使用 --scan 或 gradle dependencies 诊断。

3.3 mustRunAfter 与 shouldRunAfter 深度对比

这两个 API 都用于在 不建立依赖关系 的前提下控制任务执行顺序,但语义强度完全不同。下表从多个维度进行对比:

对比维度 mustRunAfter shouldRunAfter
语义 “必须晚于” —— 硬性排序 “尽量晚于” —— 软性提示
环检测 参与环检测,产生环则报错 不参与环检测,产生环则静默丢弃
并行抑制 强制串行化,阻止并行执行 尽量串行,但如果并行更优可忽略
典型场景 清理任务必须在编译任务之前;日志聚合必须在测试之后 代码检查尽量在测试之后,但不是必须
多模块跨项目 有效,跨模块同样强制 有效,但跨模块时可能被排序器忽略
调试标识 日志中显示 MUST_RUN_AFTER 日志中显示 SHOULD_RUN_AFTER

3.4 具体配置语法与示例

在 Groovy 构建脚本中,mustRunAfter 和 shouldRunAfter 的典型用法如下:

// 单模块示例
task clean {
    doLast {
        println 'Cleaning...'
    }
}

task compile {
    doLast {
        println 'Compiling...'
    }
}

task test {
    doLast {
        println 'Testing...'
    }
}

// 必须关系:clean 必须在 compile 之前
compile.mustRunAfter clean

// 建议关系:test 尽量在 compile 之后
test.shouldRunAfter compile

多模块跨项目配置时,需要引用其他模块的任务路径:

// 在模块 B 的 build.gradle 中
tasks.named('test') {
    // 模块 B 的 test 必须晚于模块 A 的 assemble
    mustRunAfter project(':module-a').tasks.named('assemble')
}

// 在模块 C 的 build.gradle 中
tasks.named('integrationTest') {
    // 模块 C 的 integrationTest 尽量晚于模块 B 的 test
    shouldRunAfter project(':module-b').tasks.named('test')
}

使用 Kotlin DSL 时,语法略有不同:

// build.gradle.kts
tasks.register("validate") {
    doLast { println("Validating...") }
}

tasks.register("deploy") {
    doLast { println("Deploying...") }
}

// 必须关系
tasks.named("deploy") {
    mustRunAfter("validate")
}

// 建议关系
tasks.named("validate") {
    shouldRunAfter("lint")
}
调试技巧: 使用 gradle task --all --info 可以看到每个任务的排序约束。在输出中搜索 mustRunAfter 或 shouldRunAfter 即可确认配置是否生效。如果需要更细粒度的图可视化,可以添加 --scan 生成 Build Scan 并在浏览器中查看交互式 DAG。

3.5 任务排序的并行优化考量

shouldRunAfter 的核心价值在于 允许 Gradle 在不会引起副作用的情况下突破顺序约束,从而最大化并行度。考虑以下场景:

  • 有硬件资源限制: 两台测试机共享同一个数据库,integrationTest 必须互斥运行——使用 mustRunAfter 强制串行。
  • 无资源冲突: 单元测试 test 和代码检查 lint 可以完全并行,但希望 lint 在 test 之后执行以便日志更有序——使用 shouldRunAfter,并行度不会受损。
  • 多模块聚合: 在根项目中,allprojects 或 subprojects 内声明的任务排序,推荐优先使用 shouldRunAfter,避免跨模块的强排序导致不必要的构建阻塞。
性能优化经验公式:
对于非关键路径上的排序约束,优先选择 shouldRunAfter 而非 mustRunAfter,可提升 5%~15% 的多模块并行构建吞吐量(视模块粒度而定)。只有当出现 资源锁竞争、输出文件覆盖、数据库写入冲突 等实际竞态问题时,才升级为 mustRunAfter。

3.6 常见错误与诊断清单

下表列出了任务排序配置中最常遇到的 5 类错误及其修复策略:

错误模式 典型日志 根因 修复方式
环形依赖 CircularDependencyException: Path to root A → B → A 或 mustRunAfter 闭环 检查所有 dependsOn 和 mustRunAfter 通路,剪除环
mustRunAfter 被忽略 任务执行顺序未按预期 使用了跨模块路径但未正确引用 project() 路径 改用 project(':module').tasks.named('taskName') 引用
shouldRunAfter 未生效 任务在应之后的任务之前执行 排序器检测到环并丢弃了约束 检查是否形成隐性环;如需要强制顺序应升级为 mustRunAfter
并行爆炸 大量任务同时启动,系统负载过高 缺失 mustRunAfter 导致无约束并行 对资源敏感任务添加 mustRunAfter 分组串行
配置时序错误 Task with name X not found 在任务注册之前引用了该任务 使用 tasks.named() 或 tasks.whenTaskAdded 延迟引用
避坑提醒: 不要在 afterEvaluate 块之外对 mustRunAfter / shouldRunAfter 做条件判断分支,否则可能导致部分分支下的约束未被注册。始终在 tasks.named() 或 tasks.register() 闭包内完成排序声明。

3.7 综合实战:多模块任务调度配置

以下是一个真实的多模块 Kotlin 项目部分配置,展示如何组合使用 dependsOn、mustRunAfter 和 shouldRunAfter:

// root/build.gradle.kts
subprojects {
    tasks.register("qualityCheck") {
        dependsOn("lint", "detekt")
    }

    tasks.named("test") {
        mustRunAfter("compileKotlin")
    }

    tasks.named("integrationTest") {
        shouldRunAfter("test")
        mustRunAfter("compileKotlin")
    }
}

// 模块 A 额外配置
project(":module-a") {
    tasks.named("integrationTest") {
        // 跨模块:A 的集成测试必须晚于 B 的单元测试
        mustRunAfter(project(":module-b").tasks.named("test"))
    }
}

// 模块 C 额外配置
project(":module-c") {
    tasks.named("deploy") {
        // 软性提示:deploy 尽量在 qualityCheck 之后
        shouldRunAfter(project(":module-a").tasks.named("qualityCheck"))
    }
}

执行 gradle :module-c:deploy --dry-run 可以预览最终的任务排序顺序。从日志中可以看到 compileKotlin → test(mustRunAfter)→ qualityCheck(dependsOn 链)→ deploy(shouldRunAfter) 的完整轨迹。

小结:
  • 任务图是 DAG,Gradle 通过拓扑排序确定执行顺序。
  • dependsOn 创建依赖链;mustRunAfter 创建强制排序;shouldRunAfter 创建软排序。
  • 跨模块排序必须使用 project(':path').tasks.named('name') 引用。
  • 优先用 shouldRunAfter 保持并行度,仅在资源冲突时升级为 mustRunAfter。
  • 使用 --dry-run、--info 和 --scan 诊断排序问题。