任务图优化:任务图分析、任务依赖优化、按需配置、避免常见陷阱

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 秒。

不过要注意,按需配置不是银弹。它有一些限制:

场景 是否支持按需配置 说明
单模块构建 ✅ 完全支持 效果最明显
多模块构建(依赖明确) ✅ 支持 依赖链上的模块会被配置
使用了 allprojectssubprojects ⚠️ 部分支持 这些闭包仍会执行,但内部任务不会配置
动态任务注册 ❌ 不支持 如果任务是在配置阶段动态注册的,按需配置可能失效

我个人建议,新项目从一开始就开启按需配置。老项目的话,先跑一遍 --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 {
        // 生成报告
    }
}

我曾经接手过一个项目,构建脚本里有一堆没有声明输入输出的任务。每次构建都要重新跑一遍,哪怕什么都没改。后来我花了两天时间,给每个任务都加上了 inputsoutputs,构建时间从 8 分钟降到了 2 分钟。

一个小技巧:如果你不确定某个任务是否被正确缓存了,可以加 --info 参数运行。Gradle 会打印出每个任务的缓存状态,是 UP-TO-DATEFROM-CACHE 还是 EXECUTING。看到 EXECUTING 的任务,就值得去检查一下它的输入输出声明。

任务图优化核心逻辑

下面这张图,是我总结的任务图优化核心逻辑。你可以把它当作一个检查清单:

任务图优化核心逻辑 1. 任务图分析 --dry-run / taskTree 2. 依赖优化 显式依赖 / mustRunAfter 3. 按需配置 configureondemand=true 4. 避免常见陷阱 循环依赖 / finalizedBy / up-to-date ✅ 构建速度提升 配置阶段缩短 · 增量构建生效 · 任务图清晰 检查清单 □ 跑过 --dry-run 了吗? □ 依赖是显式的吗? □ 开启按需配置了吗? □ 任务有 inputs/outputs 吗? □ 有循环依赖吗? □ finalizedBy 用对了吗?

这张图把优化流程串起来了。先分析,再优化,然后开启按需配置,最后检查陷阱。每一步都做到位了,构建速度自然就上来了。

最后说一句,任务图优化不是一劳永逸的事。项目在变,依赖在变,任务图也在变。我建议每个迭代结束后,花 10 分钟跑一遍 --dry-run,看看有没有新的问题冒出来。嗯,这点时间花得值。


公众号:蓝海资料掘金营,微信 deep3321