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) |
| 缓存命中条件 | 所有输入完全一致,包括文件内容、顺序、类型等 |
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. 升级导致缓存失效的典型场景
- Transform API 变更:不同 AGP 版本中 Transform 接口的
getScopes()、getInputTypes()返回不同集合,缓存键中的序列化对象哈希变化。 - 任务输入属性重命名:例如 AGP 8.0 将
mergeJavaResources的输入从javaResourceDir改为mergedJavaResourceDir,导致缓存键差异。 - 中间产物路径变化:
build/intermediates下的子目录结构在不同版本间重新组织,作为其他任务的输入时路径哈希改变。 - Gradle 版本升级:即使 AGP 不变,Gradle 8.x 的
BuildCacheController缓存键序列化算法较 7.x 有微小差异,也会使缓存失效。 - 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 | 删除本地缓存目录重新构建 |
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 缓存键由“源代码+配置”与“AGP/工具链环境”共同计算而成。升级会导致红色区域内的输入变化,即使蓝色区域未修改,最终缓存键也产生差异,引发全局缓存失效。