带过多模块工程的朋友,应该都体会过这种场景:只改了一行工具类代码,点一下构建,然后眼睁睁看着进度条从第一个模块开始一路编译过去,十几秒甚至几分钟就没了。这种时候所有人都会问同一个问题——Gradle 增量构建到底能不能只构建修改过的模块与代码?答案是可以,但前提是你得真正理解它怎么判断“修改过”,以及你的工程结构有没有给它判断的依据。这篇博客就是围绕这件事展开的,适合被构建速度折磨的 Android/Java 开发者,也适合刚接触 Gradle 想少走弯路的同学。
1. 多模块工程为什么越改越慢:全量构建的账要算清楚
1.1 全量构建的成本到底花在哪
一个常见的多模块工程,比如 app 壳模块依赖业务模块 A/B/C,业务模块又依赖基础库 base。平时跑一次 debug 构建,要完成 Java/Kotlin 编译、资源合并、AAR 打包、dex、lint、测试等任务。如果只改了一个 TextView 的文案,理想情况下应该只重编这个字符串所在的模块,甚至只需要重新处理资源。但很多工程实际跑起来和 clean build 差别不大,所有模块的 compile 任务全部重新执行,改动一个小常量要等好几分钟。
问题出在哪?Gradle 本身是一个任务调度框架,它不保证任何任务默认跳过。只有任务自己声明了稳定的输入输出,并且 Gradle 检测到输入输出没有变化,它才会把任务标记为 UP-TO-DATE 然后跳过。如果你的模块依赖关系太粗,或者某个任务没有声明完整的输出目录,Gradle 的增量判断就失效,退化成全量重跑。换句话说,全量构建的成本不只是来自编译器本身,更多来自调度器无法判断“哪些可以不动”。
举一个实际例子:某个任务的输出目录同时被多个任务写入,或者输出目录里混入上次构建留下的随机文件名,Gradle 对输出做快照时发现“和上次不一样”,就判定输出已变化,下次构建重跑。这类细节看似微小,在多模块工程里会被放大成几十秒甚至几分钟的无效耗时。
1.2 Gradle 不是编译器,而是任务调度器
理解增量构建,首先要放下“Gradle 是编译器”的错觉。编译器把源码变成字节码,Gradle 做的是把编译、打包、拷贝、测试这些动作编排成一张任务图,然后逐个执行。真正做增量编译的是 Javac/Kotlin 编译器内部的增量机制,Gradle 的增量构建是在任务级别做跳过判断。这是两层。
任务级别的意思是:Gradle 判断某个 Task 的输入(源文件、依赖 jar、参数)和输出(class 文件、jar、报告)相比上次执行有没有变化。没变,标记 UP-TO-DATE,不执行动作,时间接近 0。代码级别是:即使 Task 被判定为需要执行,编译器也可能只重新编译变更文件涉及的那部分源码,比如 Java 增量编译只重新编译受影响的类。
所以“只构建修改过的模块与代码”这句话,其实是两个层面叠加的效果:模块靠任务快照跳过,代码靠编译器增量能力。这两层缺一不可。任务级判断失效,编译器再聪明也没用,因为编译器拿到的是全量源码目录;代码级失效,模块虽然“跳过”成功,模块内部依然会退化成整模块重编,时间照样久。
1.3 增量构建的预期收益:不是所有场景都能跳过
这里要给预期先降降温。增量构建不是银弹,它能跳过的,是那些输入输出完全稳定、且和本次改动无关的任务。像 packageDebug、assembleDebug 这种最终打包任务,只要前面产物有变化,它必然重跑;单元测试任务如果配了“总是执行”,也不会跳过。
合理的预期是:模块级跳过能省掉与改动无关模块的编译、资源合并、打包时间;代码级增量能省掉同模块内无关类的重编时间。但最终组装、签名、安装这些步骤该跑还是跑。评判增量构建是否健康,不是看“是否零任务执行”,而是看“改动单个模块时,无关模块是否真的被跳过,改动点所在模块内部是否没有全量重编”。
这个标准听起来简单,实际工程里能稳定做到的并不多。接下来我会讲底层判断逻辑、工程化拆分方法,以及我自己踩过的一些坑。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 判断“这次到底该不该重跑”:快照机制与 UP-TO-DATE
2.1 Task 输入输出快照如何工作
Gradle 的每个 Task 都可以声明 inputs 和 outputs。执行一个 Task 之前,Gradle 会对当前 inputs 的内容指纹(文件哈希/大小/路径、属性的序列化值)和 outputs 的内容指纹做一次与上次执行完保存的指纹比对。完全一致,任务标记 UP-TO-DATE,直接跳过;不一致,执行动作,执行完保存新快照。
关键就在“完全一致”这四个字。比如你声明 inputs 为某个目录,目录里多了一个文件,指纹就变了,任务重跑。如果某个 Task 没有声明 outputs,Gradle 不知道它会产生什么,没法判断是否 UP-TO-DATE,所以每次都会执行。很多自定义 Gradle 插件偷懒,copy/zip 任务没声明输出,它们就会成为增量构建链路里的排水口。
一个形象类比:快照机制很像收快递时核对包裹清单。输入是“预计有哪些东西”,输出是“收到后箱子的状态”。如果清单和箱子状态都和上次一样,收件员直接
