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 ChangesApply Code Changes。前者会重启Activity,后者只替换方法体。能用后者就用后者,快得多。

热部署的局限性

不是所有修改都能热部署。比如:

  • 修改了资源文件(布局、图片等)——需要重启Activity
  • 修改了AndroidManifest.xml——必须重新安装
  • 新增了类或方法——部分方案支持,部分不支持

所以别指望热部署解决所有问题。该重启还是得重启。

9.6 一张图总结

说了这么多,你可能有点晕。我画了张图,把整个构建缓存的体系串起来:

Android 构建加速体系 构建三阶段:初始化 → 配置 → 执行 配置缓存 跳过配置阶段 org.gradle.configuration-cache=true 增量编译 只编译修改的文件 编译器层面优化 热部署 运行时替换代码 Apply Changes Gradle Build Cache(Task 结果缓存) 本地缓存 ~/.gradle/caches/ | 远程缓存(团队共享) 🎯 构建速度提升 50% ~ 90%

你看,整个体系是分层的。配置缓存解决“配置慢”,增量编译解决“编译慢”,Build Cache解决“重复执行慢”,热部署解决“重启慢”。每一层解决一个问题,叠加起来效果惊人。

9.7 实战建议

最后,给几个我踩坑踩出来的建议:

  1. 先开配置缓存。收益最大,改动最小。一行配置就能见效。
  2. 再检查增量编译。看看日志里有没有 Full recompilation is required,有的话排查原因。
  3. 最后上远程Build Cache。需要搭服务器,但团队收益巨大。
  4. 热部署当辅助。别依赖它,但能用的时候别客气。

曾经有个坑:我帮一个团队优化构建速度,发现他们开了配置缓存,但每次构建还是慢。查了半天,发现他们在build.gradle里用了 new Date() 生成版本号。每次配置都不一样,缓存当然失效了。解决方案是用 System.currentTimeMillis() 配合 @Input 注解声明为输入。

构建优化这件事,说白了就是“别做重复的事”。你省下来的每一秒,都是你未来摸鱼的时间。嗯,这话是我说的。


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