任务图优化:任务图分析、任务依赖优化、按需配置、避免常见陷阱
Gradle 的构建速度,说白了就藏在任务图里。
我刚开始用 Gradle 那会儿,总觉得它比 Maven 慢。后来才发现,不是 Gradle 慢,是我没把任务图管好。你想想看,一个项目几十个模块,每个模块又有一堆任务,如果依赖关系乱成一锅粥,Gradle 再聪明也救不了你。
这一章,我们就来聊聊怎么把任务图理清楚、跑得快。
任务图分析:先看清再动手
优化之前,得先知道当前的任务图长什么样。我个人习惯用两个命令来诊断:
// 查看完整的任务依赖图
./gradlew tasks --all
// 查看某个具体任务的依赖链
./gradlew :app:assembleDebug --dry-run
--dry-run 这个参数特别好用。它不会真的执行任务,只是打印出 Gradle 打算怎么跑。我在项目中遇到过好几次,明明只改了一行代码,结果跑了一大堆无关的任务。用 --dry-run 一看,原来是某个依赖配置写宽了。
还有一个更直观的方式,生成可视化的任务图:
// 在 build.gradle 里加这个插件
plugins {
id 'com.dorongold.task-tree' version '2.1.0'
}
// 然后运行
./gradlew :app:assembleDebug taskTree
这个插件会把任务依赖树打印出来,缩进格式一目了然。我建议你养成习惯,每次改动构建脚本后,都跑一遍 taskTree 看看。嗯,这能帮你提前发现很多问题。
核心原则:任务图越扁平,构建速度越快。深层次的依赖链会迫使 Gradle 串行执行,浪费多核 CPU 的能力。
任务依赖优化:别让任务等你
任务依赖优化,说白了就是两件事:减少不必要的依赖 和 让依赖关系更清晰。
先看一个常见的反面例子:
// ❌ 糟糕的做法:依赖了所有任务
task deploy {
dependsOn tasks.matching { it.name.startsWith("build") }
}
// ✅ 好的做法:只依赖真正需要的任务
task deploy {
dependsOn ':app:assembleRelease', ':server:buildImage'
}
我在项目中遇到过这样的场景:有人为了省事,用 dependsOn tasks 或者 dependsOn subprojects.tasks。结果每次跑 deploy,Gradle 都要把整个项目的任务图解析一遍。你想想看,一个 50 个模块的项目,光解析任务图就要花掉十几秒。
另一个常见的优化点是 使用 mustRunAfter 代替 dependsOn:
// 如果只是要求顺序,不需要强依赖
task testIntegration {
mustRunAfter test
}
// 这样,如果 test 没改动,Gradle 可以跳过它
// 而 dependsOn 会强制 test 必须执行
说白了,dependsOn 是「必须执行」,mustRunAfter 是「如果执行,请排在我前面」。用对地方,能省不少时间。
我的经验:尽量让任务依赖是「显式」的。不要用 dependsOn tasks 这种通配符。显式依赖虽然写起来麻烦点,但维护起来特别清晰。你三个月后回来看代码,一眼就知道谁依赖谁。
按需配置:别让 Gradle 做无用功
Gradle 的配置阶段,默认会配置所有任务。哪怕你只跑 ./gradlew :app:assembleDebug,Gradle 也会把整个项目的所有任务都配置一遍。这在大型项目里,就是灾难。
解决方案是 按需配置(Configuration On Demand):
// gradle.properties
org.gradle.configureondemand=true
开启后,Gradle 只会配置那些被当前执行路径涉及到的任务。我做过测试,一个 30 模块的项目,开启按需配置后,配置阶段从 12 秒降到了 3 秒。
不过要注意,按需配置不是银弹。它有一些限制:
| 场景 | 是否支持按需配置 | 说明 |
|---|---|---|
| 单模块构建 | ✅ 完全支持 | 效果最明显 |
| 多模块构建(依赖明确) | ✅ 支持 | 依赖链上的模块会被配置 |
使用了 allprojects 或 subprojects |
⚠️ 部分支持 | 这些闭包仍会执行,但内部任务不会配置 |
| 动态任务注册 | ❌ 不支持 | 如果任务是在配置阶段动态注册的,按需配置可能失效 |
我个人建议,新项目从一开始就开启按需配置。老项目的话,先跑一遍 --dry-run 看看配置阶段的时间,如果超过 5 秒,就值得开启。
注意:按需配置和 buildSrc 配合使用时,要小心。如果 buildSrc 里的插件用到了 allprojects,按需配置可能不会触发这些插件的配置。我曾经在这个坑里爬了半天,最后发现是 buildSrc 里有个插件在 allprojects 里注册了任务。
避免常见陷阱
做任务图优化,有几个坑我踩过,你最好也记住。
陷阱一:循环依赖
Gradle 不允许循环依赖,但有时候它会以一种很隐晦的方式出现:
// 模块 A 的 build.gradle
task taskA {
dependsOn ':B:taskB'
}
// 模块 B 的 build.gradle
task taskB {
dependsOn ':A:taskA'
}
这种循环依赖,Gradle 会在配置阶段直接报错。但有一种更隐蔽的情况:通过 taskGraph.whenReady 回调引入的循环依赖。我遇到过一回,排查了整整一个下午。
陷阱二:过度使用 finalizedBy
finalizedBy 是个好东西,但用多了会让任务图变得混乱:
// ❌ 每个任务都加 finalizedBy
task compileJava {
finalizedBy 'clean'
}
task compileKotlin {
finalizedBy 'clean'
}
// ✅ 只在关键路径上使用
task deploy {
finalizedBy 'cleanup'
}
你想想看,如果每个任务都 finalizedBy 另一个任务,那 Gradle 在执行时就得不断检查「这个任务执行完了,要不要触发别的?」。任务图会变得又大又复杂。
陷阱三:忽略 up-to-date 检查
Gradle 的增量构建能力,依赖于任务的输入输出声明。如果你没有正确声明,Gradle 就不知道任务是不是最新的:
// ❌ 没有声明输入输出
task generateReport {
doLast {
// 生成报告
}
}
// ✅ 声明输入输出,让 Gradle 可以判断是否 up-to-date
task generateReport {
inputs.file('data.json')
outputs.file('report.html')
doLast {
// 生成报告
}
}
我曾经接手过一个项目,构建脚本里有一堆没有声明输入输出的任务。每次构建都要重新跑一遍,哪怕什么都没改。后来我花了两天时间,给每个任务都加上了 inputs 和 outputs,构建时间从 8 分钟降到了 2 分钟。
一个小技巧:如果你不确定某个任务是否被正确缓存了,可以加 --info 参数运行。Gradle 会打印出每个任务的缓存状态,是 UP-TO-DATE、FROM-CACHE 还是 EXECUTING。看到 EXECUTING 的任务,就值得去检查一下它的输入输出声明。
任务图优化核心逻辑
下面这张图,是我总结的任务图优化核心逻辑。你可以把它当作一个检查清单:
这张图把优化流程串起来了。先分析,再优化,然后开启按需配置,最后检查陷阱。每一步都做到位了,构建速度自然就上来了。
最后说一句,任务图优化不是一劳永逸的事。项目在变,依赖在变,任务图也在变。我建议每个迭代结束后,花 10 分钟跑一遍 --dry-run,看看有没有新的问题冒出来。嗯,这点时间花得值。