AGP 版本升级概述与缓存兼容性核心问题

AGP(Android Gradle Plugin)每次大版本升级都会调整构建任务的输入/输出模型、Transform 管线和缓存键规则。本章从 Gradle 构建缓存底层原理出发,拆解 AGP 的缓存键生成逻辑,并梳理升级后最常见的缓存失效场景。为后续章节的迁移方案、调试工具和性能调优建立概念基础。

1. Gradle 构建缓存原理

Gradle 的构建缓存(Build Cache)是一种基于内容寻址的增量优化机制。每个任务(Task)的输出由一组输入决定,输入包括源代码、配置文件、环境变量、其他任务的输出等。Gradle 将输入集合序列化并计算哈希值作为缓存键(Cache Key)。当相同缓存键命中时,直接从缓存中恢复输出,跳过任务执行。

缓存组件说明
缓存键(Cache Key)输入属性的规范化序列 + SHA-256 摘要
缓存条目(Cache Entry)任务输出的压缩包,包含文件、目录和元数据
缓存存储本地(~/.gradle/caches/build-cache-1)或远程(HTTP/REST)
缓存命中条件所有输入完全一致,包括文件内容、顺序、类型等
关键认知:缓存失效不是“错误”,而是输入发生变化时的正常保护行为。AGP 升级带来的隐患在于某些隐含输入(如 Gradle API 版本、工具链路径)被纳入缓存键,导致非业务代码变更触发全部缓存失效。

2. AGP 缓存键规则

AGP 使用自定义的 Transform 任务和 Artifact 变体系统生成构建产物。每个 AGP 版本内部会为以下核心任务定义输入集:

  • processXxxResources — 资源合并与 AAPT2 编译
  • compileXxxJavaWithJavac / compileXxxKotlin — 源码编译
  • dexXxx — D8/R8 DEX 转换
  • mergeXxxNativeLibs — native 库合并

缓存键的计算受以下因素直接影响:

输入类别示例升级时是否变化
源代码内容src/main/java/*.kt否(仅开发者修改时变)
构建配置build.gradle.kts可能(AGP DSL 变更)
AGP 自身属性com.android.tools.build:gradle:8.x 版本号是
Gradle 运行时Gradle 版本、JDK 路径是
环境变量ANDROID_HOME, JAVA_HOME通常不变
其他 task 输出依赖库的 AAR 缓存路径可能(AGP 修改产物布局)

AGP 从 7.0 开始引入“变体感知”(Variant Aware)缓存,将 transformClassesWith 等任务的输入模式改为更细粒度的 @InputFiles + @Input 组合。在 8.x 中进一步将 AAPT2 编译任务与 R8 去重任务分离,导致旧缓存键格式与新版本完全不兼容。

3. 升级导致缓存失效的典型场景

  1. Transform API 变更:不同 AGP 版本中 Transform 接口的 getScopes()、getInputTypes() 返回不同集合,缓存键中的序列化对象哈希变化。
  2. 任务输入属性重命名:例如 AGP 8.0 将 mergeJavaResources 的输入从 javaResourceDir 改为 mergedJavaResourceDir,导致缓存键差异。
  3. 中间产物路径变化:build/intermediates 下的子目录结构在不同版本间重新组织,作为其他任务的输入时路径哈希改变。
  4. Gradle 版本升级:即使 AGP 不变,Gradle 8.x 的 BuildCacheController 缓存键序列化算法较 7.x 有微小差异,也会使缓存失效。
  5. JDK 或工具链版本:AGP 的 JdkToolchain 元数据被加入到部分任务的输入,导致 CI 与本地开发环境缓存不共用。

4. Gradle 构建缓存配置与命令行操作

Gradle 全局缓存配置位于 gradle.properties:

org.gradle.caching=true
org.gradle.caching.debug=false
# 远程缓存示例
org.gradle.caching=http://build-cache.internal:8080/cache/

常用命令行选项:

# 启用缓存(默认读取 gradle.properties)
./gradlew assembleDebug --build-cache

# 强制重新执行所有任务,忽略缓存
./gradlew assembleDebug --rerun-tasks

# 查看缓存命中详情(需要 BUILD_CACHE 日志级别)
./gradlew assembleDebug --info 2>&1 | grep -i "cache"

5. 构建日志分析:定位缓存未命中

使用 --info 级别输出可以看到每个任务的缓存键和命中结果。例如:

> Task :app:processDebugResources
Caching disabled for task ':app:processDebugResources' because:
  Reason: Task has no cacheable outputs because output property 'reportFile' is not annotated...

对于支持缓存的任务,日志会打印:

Task :app:compileDebugKotlin
Cache key for task ':app:compileDebugKotlin': 9a4b2c7e1f...
No cached result found for cache key 9a4b2c7e1f...

调试技巧:将缓存键值取前 12 位,分别在升级前后运行同一份源代码,若键值不一致,则说明缓存兼容性被破坏。可使用 --debug 进一步输出输入序列化细节。

6. 常见缓存兼容性问题与调试方案

问题表现调试命令/方法
AGP 版本升级后所有任务缓存全未命中构建时间骤增比较同一源代码在旧/新 AGP 下的 cache key
仅部分任务缓存失效只有 transform/merge 类任务重新执行检查 build/outputs/logs/manifest-merge-report.txt 差异
远程缓存命中但与本地输出不一致构建产物不符合预期对比 build/intermediates 文件哈希,确认输入序列化是否稳定
缓存文件损坏/不能解压Gradle 报 CacheEntryCorruptedException删除本地缓存目录重新构建
警告:不要在生产 CI 中混合使用不同 AGP 版本的远程缓存。必须为每个 AGP 版本独立缓存存储桶或使用版本隔离前缀。否则会出现缓存毒化(Cache Poisoning),导致构建明明失败却恢复成功产物。

7. 性能优化与验证步骤

  • 优化原则:减少不必要的输入变化。将 AGP 版本号、Gradle 版本号、JDK 路径等稳定信息固化到 buildCacheKey 之外的自定义缓存分组。
  • 标准验证步骤:
# 步骤 1:清空本地缓存
rm -rf ~/.gradle/caches/build-cache-1

# 步骤 2:使用旧 AGP 构建并记录缓存键
./gradlew assembleDebug --build-cache --info > old_build.log

# 步骤 3:升级 AGP 版本(修改 build.gradle.kts)
# 步骤 4:同一份源代码再次构建并记录缓存键
./gradlew assembleDebug --build-cache --info > new_build.log

# 步骤 5:比较两个日志中的 Cache key 前缀
grep 'Cache key for' old_build.log | awk '{print $NF}' | sort > old_keys.txt
grep 'Cache key for' new_build.log | awk '{print $NF}' | sort > new_keys.txt
diff old_keys.txt new_keys.txt
# 若存在差异,说明该任务缓存不兼容

若所有任务的缓存键均一致,则可确认当前升级不会导致缓存全局失效。

8. 内联 SVG:AGP 缓存键生成与升级失效流程

AGP 构建缓存键生成与升级失效流程 源代码 + 配置 .kt/.java/build.gradle AGP/Gradle 环境 版本号/JDK/工具链 输入规范化 & 序列化 属性排序 + SHA-256 缓存键: 9a4b2c7e1f... 本地/远程缓存 命中/未命中 AGP 升级的影响 • 红色区域输入变化 → 缓存键不同 • 即使源代码不变,序列化结果也改变 • 所有依赖该输入的任务缓存全部失效

上图直观展示了 AGP 缓存键由“源代码+配置”与“AGP/工具链环境”共同计算而成。升级会导致红色区域内的输入变化,即使蓝色区域未修改,最终缓存键也产生差异,引发全局缓存失效。