2、构建缓存基础:什么是构建缓存?为什么重要?

做 Android 构建,最怕什么?我个人觉得,最怕的就是每次改一行代码,整个项目就要全量重编。二十万行的工程,每次等编译都得去泡杯咖啡,回来还没好。嗯,缓存就是为了解决这个问题的。

说白了,构建缓存就是把上一次构建中 没变化的部分 保存下来,下次直接复用,省掉重复计算。你想想看,一个模块里有上百个 Java 文件,你只改了一个,其他九十九个根本没必要重新编译。缓存帮我们认出了这些“没变”的模块,直接拿上次的结果用,速度自然就上去了。

核心定义:构建缓存是一个键值存储系统,键由输入文件的内容哈希、任务配置参数等构成,值是任务的输出产物(如 .class 文件、.dex 文件、APK 片段等)。当所有输入都没有变化时,缓存命中,任务跳过执行。

2.1 构建缓存为什么重要?

我从几个最实际的场景讲起吧。

  • 加速迭代:我以前带的一个项目,300 多个模块,全量构建需要 12 分钟。开了 Gradle 构建缓存后,增量构建缩短到 40 秒。团队每天能多做两次集成,效率翻倍。
  • 节省 CI 机器成本:CI 服务器如果每次跑全量,等排队都能等半小时。缓存命中率高的话,构建时间砍半,机器资源也省了,老板看了都开心。
  • 提高开发体验:谁都不想等编译。早上来了改个 bug 改了三行代码,一编译等了五分钟,心态就崩了。缓存让“改完即跑”成为可能。

一个小对比:假设一个模块有 100 个源文件,有缓存时只编译改动的 1 个文件 + 链接;没缓存时编译 100 个。这里面的时间差距,随着项目增大是指数级的。

2.2 缓存是怎么工作的?

其实原理不复杂。每个 Gradle Task 都可以声明它的 输入 和 输出。Gradle 把输入(源文件、配置、依赖版本等)算出一个哈希值,作为缓存的 key。输出(编译产物)就是 value。

// 一个简单的 Task 输入输出示例
tasks.register<JavaCompile>("compileKotlin") {
    inputs.file("src/main/kotlin/App.kt")   // 输入文件
    inputs.property("kotlinVersion", "1.9.0") // 输入参数
    outputs.file("build/classes/main/App.class") // 输出文件
}

当第二次构建时,Gradle 重新计算输入的哈希,如果 hash 一致,就直接从缓存里拿上一次的输出,这个 Task 标记为 FROM-CACHE,不再执行。整个流程就像下面这张图:

构建缓存核心流程图 源代码 & 配置 计算输入哈希值 缓存命中 缓存未命中 直接复用缓存产物 执行实际编译任务 构建产物(APK/DEX) ? 输入不变 = 哈希不变 哈希不变 = 缓存命中

刚才那张图里,我刻意把未命中分支和命中分支分开。实际项目里,命中率越高,绿色分支跑得越频繁,构建速度就越快。未命中分支则会进入真正的编译流程,然后把结果写入缓存供下次使用。

2.3 缓存的失效条件

缓存也不是万能的。哪些情况会导致缓存失效呢?我列了个表,一目了然:

失效原因 说明 常见场景
源文件变化 任何 .kt / .java / res 文件被修改 开发日常改代码
依赖版本变化 Gradle 依赖的库升了版本 升级 Retrofit 版本
编译器参数变化 比如 compileSdk 从 34 改为 35 迁移 AGP 版本前后
自定义 Task 输入声明不全 漏写了某输入,导致缓存 key 与实际输入不匹配 自己写的编译插件没加 inputs
构建环境变化 JDK 版本、操作系统变了 CI 机器从 Linux 换成 macOS

我曾遇到的一个坑:有一个模块,每次改一行代码都要重编整个子模块,我以为缓存坏了。查了半天发现,那个模块的 Kotlin 编译 Task 里用了 inputs.dir 但没有排除 build 目录,导致 build 目录里生成的时间戳文件作为输入被计算到哈希里,每次构建时间戳都变,所以缓存永远不命中。把输入范围精确到 src 目录就解决了。

2.4 本地缓存 vs 远程缓存

Gradle 构建缓存分两种:

  • 本地缓存:默认开启,存在你电脑的 ~/.gradle/caches/build-cache-1 目录下。只对自己有用,团队里其他成员无法共享。
  • 远程缓存:需要配置 HTTP 服务(如 Gradle Enterprise、Artifactory、S3 等)。团队所有机器共享一份缓存,CI 产出的缓存可以被开发者直接复用,节省大量时间。

我个人建议小团队可以先从本地缓存开始,等 CI 构建时间成了瓶颈,再上远程缓存。远程缓存的配置也不复杂,加一行就够:

// gradle.properties
org.gradle.caching=true
// 如果有远程缓存服务
org.gradle.cache.tasks.http.url=https://build-cache.mycompany.com/

2.5 缓存对 AGP 版本升级的意义

好,回到咱们这门课的主题。AGP 版本升级往往是系统工程,涉及 Gradle 版本、Kotlin 版本、各种插件版本一起变。一旦升级,很多缓存 key 会被破坏,因为编译器参数变了,输入哈希也变了。第一次构建可能全量运行,但第二次构建就应该恢复增量。

你想想看,如果升级后某个模块的缓存死活不命中,每次改一行都要花 2 分钟等它编译,那这个版本升级就基本失败了。我们要做的,就是通过瀑布图(后面章节会详细讲)把这些“断了缓存”的模块一个个揪出来,分析为什么缓存没命中,然后修复。

一个实用的检查命令:在 Gradle 构建的时候加上 --info 参数,日志里会打印每个 Task 的状态为 FROM-CACHE 还是 EXECUTING。一眼就能看出哪个模块吃到了缓存,哪个没有。

好的,构建缓存的基础内容就这些。总结一句话:缓存就是靠输入哈希来跳过重复工作。理解了这个,你后面看瀑布图就顺了。