一、AGP构建缓存概述:什么是构建缓存,为什么需要优化

各位开发者朋友,今天我们来聊聊 Android 构建里一个绕不开的话题——AGP 构建缓存。我刚开始带团队那会儿,有个项目每次 clean build 都要 15 分钟,同事们都抱怨「改一行代码喝三杯咖啡」。后来我花了整整两天研究 Gradle 缓存机制,把构建时间压到了 3 分钟以内。这个经历让我深刻意识到:搞懂构建缓存,是每个 Android 工程师进阶的必经之路。

1.1 什么是构建缓存?

说白了,构建缓存就是把编译过程中产生的中间产物存起来。下次只要你没改相应的源文件,就直接拿来用,不用重新干活。

举个例子:你改了某个 Java 类里的一个 log 打印,如果没有缓存,Gradle 会重新编译整个模块的所有 Java 文件、重新执行 AAPT2 处理资源、重新混淆……一套流程下来几分钟没了。但如果你开启了构建缓存,它只会重新编译你改过的那一个文件,别的文件直接复用上一次的编译结果。——嗯,就是这么简单。

核心概念快速理解

概念解释个人类比
构建缓存保存编译中间产物(.class、.dex、资源包等)就像厨房里提前切好的配菜
缓存键根据输入文件路径、内容哈希、任务参数生成的唯一标识配菜包装上的条形码
缓存命中下次构建时发现缓存键匹配,直接读取产物直接拿配菜下锅
缓存失效输入发生变化时缓存键改变,需重新执行任务配菜过期了,得重新切
增量编译只重新编译变化文件及依赖它的文件只炒需要回锅的菜

你可能会问:缓存这玩意不是 Gradle 自带的吗?还需要专门优化?我说个真事——我曾经接手过一个项目,build.gradle 里有 30 多个模块,每个模块都写死了 android.enableBuildCache=false。问当时负责的人,他说「因为开启了缓存后经常出奇怪的问题,干脆关了省心」。这其实就是典型的「用不好就弃疗」的例子。

1.2 为什么需要优化构建缓存?

我总结了三个最痛的点,你可以对照看看项目有没有中招:

  • 慢——构建时间动不动 10 分钟+:尤其是大项目,随便改个资源文件就要全量重跑。我见过最夸张的一次,某 app 全量构建用了 47 分钟,那天下午整个组都在等编译完成。
  • 疼——增量编译经常失效:明明只改了一个字符,Gradle 却重新编译了半个工程。我排查过这类问题,多数是因为 annotationProcessor 配置不当,或者 BuildConfig 字段被频繁修改。
  • 坑——缓存导致构建结果不一致:本地跑没问题,CI 上却报错。这种情况十有八九是本地缓存和 CI 缓存不一致,或者混淆映射文件被缓存了错误版本。我之前就掉进过这个坑,排查了整整两天,最后发现是 proguard-rules.pro 的修改没触发缓存失效。

⚠️ 避坑指南: 我曾经在项目里直接把 .gradle 缓存目录做成共享目录,结果多个分支同时构建时缓存互相污染,构建产物张冠李戴。后来我学乖了——不同分支必须用独立的缓存目录,或者直接清空缓存再构建。

1.3 AGP 构建缓存的体系结构

AGP 的缓存体系其实分两层:Gradle 任务输出缓存 和 AGP 自身的中间产物缓存。它们之间的关系就像这样:

AGP 构建缓存体系结构 Gradle 构建缓存(Task Output Cache) 将每个 Gradle Task 的输出按缓存键存储 缓存位置:~/.gradle/caches/ 或自定义目录 支持本地缓存 & 远程缓存(Caching Relocation) AGP 缓存模块(中间产物缓存) AAPT2 编译缓存(.flat 文件) DEX 合并缓存(.dex 中间切片) 资源混淆映射缓存(resource mapping) 开发者配置层 gradle.properties 启用缓存:android.enableBuildCache=true 构建变体维度隔离、缓存目录设置等 关键关系 下层缓存失效 →上层重新执行 上层配置影响 下层缓存策略

从图上你可以看出来,底层是 Gradle 本身的 Task 缓存,它负责管理所有任务的输出。往上一层是 AGP 实现的特殊缓存,比如 AAPT2 编译出来的 .flat 文件、Dex 拆分后的中间 dex。我个人的习惯是——如果只想快速生效,优先调 AGP 这层配置,因为它离资源、代码更近,缓存命中率也更高。

1.4 缓存优化的核心指标

我们做优化不能凭感觉,得有数据。我一般盯着这三个指标看:

  1. 缓存命中率(Cache Hit Ratio):理想值应该在 85% 以上。如果低于 60%,说明你的构建配置有问题,缓存基本没起作用。
  2. 增量编译时间(Incremental Build Time):改一个 Java 文件后全部重新构建的时间,应该控制在 30 秒以内。超过 1 分钟就需要排查了。
  3. 缓存无效清除次数(Cache Invalidation Count):每次构建时有多少个任务因为缓存失效而重跑。这个数字如果经常超过 20,就要看看是不是有全局的输入变化(比如时间戳污染)。

💡 我的实测经验: 有一次我发现项目里有个模块的 buildConfigField 里放了 BUILD_TIME,每次编译都会生成不同的时间戳,导致依赖它的所有模块缓存全部失效。去掉这个字段后,缓存命中率从 32% 直接飙升到 91%。你看,改动点往往很小,效果却立竿见影。

1.5 常见的缓存失效原因

我总结了一份「缓存失效黑名单」,你对照排查一下:

失效原因触发场景解决方案
全局时间戳或随机值BuildConfig 里放 VersionCode / BUILD_TIME改用 git 提交计数,移除时间戳
资源文件频繁变动每次构建都修改 res/values/strings.xml建立只读的资源基线,通过 flavor 隔离
注解处理器未声明输入使用了 Lombok / Room 等但不配置 incremental在 processor 参数里加 incremental=true
构建变体维度太多flavorDimensions + buildTypes 产生 20+ 变体裁剪变体,或使用 Gradle 属性控制只构建指定变体
跨项目依赖未缓存依赖本地 aar 或 module 时 hash 计算有偏差统一使用 Maven 本地仓库,避免直接依赖路径

你看,其实很多问题并不是 AGP 本身不行,而是我们在日常开发中不经意间破坏了缓存机制。我经常跟团队说一句话:「缓存不是万能的,但不懂缓存是万万不能的。」