9、构建缓存与增量构建:构建缓存原理、Gradle Build Cache、配置缓存(Configuration Cache)、增量编译与热部署
说实话,做Android开发最怕什么?不是bug,是等编译。
项目大了以后,一次clean build喝杯咖啡都回不来。我见过最夸张的,一个模块化项目全量构建要跑12分钟。12分钟啊,够你刷一集短视频了。
所以这一章,咱们聊聊怎么让构建“快起来”。核心就四个字:能省则省。
9.1 构建缓存的核心原理
先想一个问题:你改了一行代码,凭什么要把所有代码重新编译一遍?
构建缓存的思路很简单——把上一次构建的结果存起来。下次构建时,如果输入没变,就直接拿缓存结果,跳过执行。
这就像你做饭。今天炒的番茄炒蛋和昨天一模一样,那你还重新切番茄、打鸡蛋吗?直接把昨天做好的热一热不就行了。
缓存的关键:判断“输入”是否变化。
对于Gradle来说,每个Task都有明确的输入和输出。输入变了,Task才重新执行。输入没变,直接拿输出。
我在项目中遇到过一个问题:明明只改了一个资源文件,结果整个项目重新编译了。查了半天,发现是某个Task把时间戳当成了输入。每次构建时间戳都不一样,所以缓存永远命中不了。嗯,这是个坑,后面会细说。
9.2 Gradle Build Cache
Gradle Build Cache 是官方提供的缓存机制。它分两种:
- 本地缓存:默认开启,存在
~/.gradle/caches/下 - 远程缓存:需要配置,适合团队共享构建结果
说白了,本地缓存只对你自己有用。远程缓存才牛逼——同事A编译过的模块,同事B直接拿结果,不用重新编译。
怎么开启?
在 gradle.properties 里加一行:
org.gradle.caching=true
远程缓存需要配置 buildCache 块:
// settings.gradle
buildCache {
local {
enabled = true
}
remote(HttpBuildCache) {
url = 'https://your-cache-server/'
push = true
}
}
我的建议:团队超过5个人,一定要上远程缓存。我见过一个团队,每天每个人都在重复编译相同的代码,浪费的时间加起来够开发一个新功能了。
缓存命中率怎么查?
加个参数跑一次构建:
./gradlew assembleDebug --build-cache -Dorg.gradle.caching.debug=true
日志里会显示每个Task是 FROM-CACHE 还是 EXECUTING。如果全是 EXECUTING,说明缓存没起作用,得排查了。
9.3 配置缓存(Configuration Cache)
这个功能是Gradle 6.6引入的,我愿称之为“近十年Gradle最香的优化”。
为什么?因为Gradle构建分三个阶段:初始化 → 配置 → 执行。配置阶段要解析所有build.gradle,生成Task图。项目大了以后,光配置阶段就能跑30秒。
配置缓存就是把这个阶段的结果存下来。下次构建时,如果配置没变,直接跳过配置阶段。
注意:配置缓存和Build Cache是两码事。Build Cache缓存的是Task的执行结果,配置缓存缓存的是Task的配置状态。两者可以同时使用,效果叠加。
开启方式
// gradle.properties
org.gradle.configuration-cache=true
第一次开启时,构建会慢一些(因为要生成缓存)。第二次开始,你会明显感觉到“快了很多”。
我曾经在一个200+模块的项目上试过。开启前,配置阶段耗时28秒。开启后,配置阶段直接降到2秒。你想想看,每次构建省下26秒,一天构建20次,就是8分多钟。一个月下来,省出来的时间够你学一门新技术了。
踩坑记录
配置缓存有个问题:它要求所有Task的输入输出都是可序列化的。如果你在Task里用了匿名内部类、闭包、或者不可序列化的对象,就会报错。
我曾经遇到一个插件,在Task里用了 java.util.Date 作为输入。配置缓存一开,直接崩了。解决方案是把Date换成 String 或者 Long 时间戳。
9.4 增量编译
增量编译是编译器层面的优化。说白了就是:你改了一个Java文件,编译器只重新编译这个文件,以及依赖它的文件。
Gradle默认开启了Java和Kotlin的增量编译。但有个前提——你的代码要写对。
增量编译失效的常见原因:
- 使用了APT(注解处理器)且没有声明增量支持
- Task的输入声明不完整(比如漏了某个依赖)
- 使用了
outputs.upToDateWhen { false }强制每次都执行
我见过最离谱的:有人在build.gradle里写了 outputs.upToDateWhen { false },理由是“确保每次都是最新的”。结果整个项目每次都是全量编译,他还抱怨Gradle慢。嗯,这锅Gradle不背。
怎么确认增量编译生效了?
看构建日志。如果看到 Executing incremental compilation,说明生效了。如果看到 Full recompilation is required,说明没生效。
9.5 热部署
热部署,也叫热替换。它和增量编译的区别在于:增量编译是“编译时”的优化,热部署是“运行时”的优化。
你改了一行代码,热部署可以直接把修改后的类加载到正在运行的App里,不用重启App。
Android里的热部署方案
| 方案 | 原理 | 适用场景 |
|---|---|---|
| Instant Run(已废弃) | 方法替换 | 早期方案,不推荐 |
| Apply Changes | Android Studio自带,基于JVM TI | 日常开发,推荐使用 |
| JRebel for Android | 字节码增强 | 大型项目,付费方案 |
我个人习惯用 Apply Changes。Android Studio 3.5以后,这个功能已经很稳定了。改个UI、调个逻辑,基本秒级生效。
小技巧:Apply Changes 分两种:Apply Changes 和 Apply Code Changes。前者会重启Activity,后者只替换方法体。能用后者就用后者,快得多。
热部署的局限性
不是所有修改都能热部署。比如:
- 修改了资源文件(布局、图片等)——需要重启Activity
- 修改了AndroidManifest.xml——必须重新安装
- 新增了类或方法——部分方案支持,部分不支持
所以别指望热部署解决所有问题。该重启还是得重启。
9.6 一张图总结
说了这么多,你可能有点晕。我画了张图,把整个构建缓存的体系串起来:
你看,整个体系是分层的。配置缓存解决“配置慢”,增量编译解决“编译慢”,Build Cache解决“重复执行慢”,热部署解决“重启慢”。每一层解决一个问题,叠加起来效果惊人。
9.7 实战建议
最后,给几个我踩坑踩出来的建议:
- 先开配置缓存。收益最大,改动最小。一行配置就能见效。
- 再检查增量编译。看看日志里有没有
Full recompilation is required,有的话排查原因。 - 最后上远程Build Cache。需要搭服务器,但团队收益巨大。
- 热部署当辅助。别依赖它,但能用的时候别客气。
曾经有个坑:我帮一个团队优化构建速度,发现他们开了配置缓存,但每次构建还是慢。查了半天,发现他们在build.gradle里用了 new Date() 生成版本号。每次配置都不一样,缓存当然失效了。解决方案是用 System.currentTimeMillis() 配合 @Input 注解声明为输入。
构建优化这件事,说白了就是“别做重复的事”。你省下来的每一秒,都是你未来摸鱼的时间。嗯,这话是我说的。
公众号:蓝海资料掘金营,微信deep3321