任务图与执行顺序
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 任务图构建核心流程的示意:
- 解析(Parsing): 读取所有模块的
build.gradle/build.gradle.kts,注册每个任务定义。 - 链接(Linking): 遍历
dependsOn、mustRunAfter等声明,在任务节点之间建立有向边,同时检测环。 - 排序(Sorting): 对 DAG 执行拓扑排序,生成最终的线性执行序列;可并行执行的任务会被标记为同一批次。
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诊断排序问题。