缓存同步场景与问题:模块间缓存不一致、增量构建失效、缓存污染、远程缓存配置要点
在多模块 Gradle 项目中,AGP 版本升级后,构建缓存(Build Cache)的同步问题会急剧放大。缓存不一致、增量构建失效、缓存污染以及远程缓存配置错误,会使原本只需几秒的增量构建退化为全量构建,甚至引入隐藏的编译错误。
本章从缓存同步的底层原理出发,梳理四种典型问题场景的触发条件,给出排查手段和修复方案,并重点说明远程缓存在多模块项目中的正确配置要点。
4.1 构建缓存同步原理
Gradle 的 Build Cache 基于输入哈希(Input Hash)与输出快照(Output Snapshot)实现缓存命中。每个 Task 的输入(文件、属性、action 实现类等)被序列化为哈希值,当哈希值与缓存记录一致时,Task 跳过执行,直接恢复之前输出的文件。
在多模块项目中,每个模块的 Task 依赖关系形成 DAG(有向无环图),AGP 升级后常见的缓存失效原因包括:
- 插件版本变更:AGP 版本号作为 BuildConfig 的一部分,会改变部分 Task 的输入属性。
- 模块间输出依赖:模块 A 的输出文件路径或签名方式变化,导致模块 B 的输入哈希失效。
- 缓存 key 冲突:同一 Task 因
outputs.cacheIf条件不同,缓存 key 不一致。 - 远程缓存元数据:未正确配置缓存的储存格式或压缩算法,导致拉取后解压异常。
--build-cache --rerun-tasks 一次,重新预热缓存。
4.2 模块间缓存不一致
模块间缓存不一致是 AGP 升级后最隐蔽的问题。典型场景:模块 :feature-a 升级了 AGP 版本(或使用了新的编译 option),而模块 :core 的缓存仍基于旧版本生成。当 :feature-a 依赖 :core 的 AAR 或 classes.jar 时,Gradle 通过 ConsistentCopySpec 检测文件差异,触发全量 recompile。
排查手段:
- 执行
gradle assembleDebug --build-cache --scan生成构建扫描(Build Scan)。 - 在扫描页面中查看“Task Cache Hit Ratio”,找出缓存命中率为 0% 的 Task。
- 对比同一 Task 在前后两次构建中的 input hash 变化:
gradle :core:compileReleaseKotlin --build-cache --info 2>&1 | grep "Cache hit\|Cache miss\|input hash"
修复方案:
- 对所有模块执行
cleanBuildCache并重建:gradle clean cleanBuildCache assembleDebug --build-cache - 在
gradle.properties中统一 AGP 版本号,并确保所有模块的compileSdk、minSdk、targetSdk一致。 - 对跨模块的输出文件使用
NormalizedPathProperty或RelativeBuildCache标准化路径。
| 缓存不一致诱因 | 影响范围 | 快速验证命令 |
|---|---|---|
| 模块间 compileSdk 不一致 | 所有依赖该模块的 Task | gradle dependencies --info |
| 不同的混淆配置(R8/ProGuard 版本) | transformClassesAndResourcesWithR8 | 对比 build/intermediates/optimized 文件 |
动态 versionName 或 versionCode |
生成 BuildConfig 的 Task | gradle :app:packageRelease --build-cache --stacktrace |
4.3 增量构建失效
增量构建失效表现为:即使只修改了一个源文件,整个模块甚至整个项目都被重新编译。在 AGP 升级后,常见的失效原因包括:
- Annotation Processor 改变输入:新版本 AGP 可能改变了某些 Processor 的输入文件列表(例如 Room 的 schema export 路径)。
- BuildConfig 字段生成:AGP 8.x 将部分 BuildConfig 字段(如
FLAVOR)改为动态生成,导致每次构建 input hash 都会变化。 - 资源缩减(resource shrinking):新版本 R8 可能将
keep.xml视为输入,而keep.xml的修改会触发 resources-optimize Task 重跑。
调试技巧:
使用 --profile 生成构建性能报告,查看每个 Task 的执行时间及是否被 FROM_CACHE 跳过。如果发现某个 Task 的 up-to-date 状态始终为 false,检查其输入属性:
gradle :app:processReleaseResources --output-file build/reports/resources-up-to-date.txt
cat build/reports/resources-up-to-date.txt | grep "InputProperty"
gradle.properties 中设置 org.gradle.caching=true 并添加 org.gradle.caching.debug=true,可在构建日志里看到每个 Task 的缓存 key 和命中结果,便于快速定位失效 Task。
4.4 缓存污染
缓存污染指缓存中存储了“脏”输出(如未完成编译的中间产物、带有路径的文件),导致后续构建即使输入正确,也恢复出错误结果。AGP 升级后,模块间 proguardMapping 文件或 buildConfig 的缓存污染尤其常见。
污染来源:
- 多个 CI 环境使用不同的
user.home路径,缓存未使用相对路径(pathSegments)。 - Task 使用了
@OutputFile但未声明@Destroys或@Cleanup,导致输出残留。 - Kotlin 的 Incremental Compilation 与 Build Cache 冲突:部分 Kotlin 编译的
@InputFiles未正确设置pathSensitivity。
治理方法:
- 定期自动清理:在 CI 流水线中,每次 AGP 升级后执行:
gradle cleanBuildCache rm -rf ~/.gradle/caches/ # 慎用,会删除所有缓存 - 使用安全缓存 key:为每个 Task 明确指定
outputs.cacheIf { true }并添加@CacheableTask注解。避免在 Task 中直接操作buildDir之外的路径。 - 引入缓存黑名单:在
settings.gradle.kts或buildSrc中指定不参与缓存的文件模式:buildCache { remote { isEnabled = false isPush = false } local { directory = File("${System.getProperty("user.home")}/.gradle/caches/build-cache-1") removeUnusedEntriesAfterDays = 7 } }
~/.gradle/caches/ 全局清理作为常规操作。这会导致所有本地项目缓存丢失。推荐仅清理 ~/.gradle/caches/build-cache-* 目录,保留其他依赖缓存。
4.5 远程缓存配置要点
多模块项目在 CI 或团队协作中,常使用远程缓存(如 Gradle Enterprise 或自定义 HTTP 缓存)。AGP 升级后,远程缓存配置须注意以下要点:
| 配置项 | 作用 | AGP 升级后检查 |
|---|---|---|
allowUntrustedServer |
允许连接自签名证书的远程缓存 | 如果升级了 Gradle 版本,自签名证书链可能失效,需更新 TrustStore |
credentials |
远程缓存认证凭据 | AGP 8.x 后 BuildCacheController 对凭据的读取方式变化,需指定 username/password 或 accessKey |
compression |
缓存压缩算法(gzip / deflate) | 确保 CI 和开发环境使用相同压缩算法,否则解压异常会导致缓存 miss |
storeMode |
远程缓存存储模式(MERGED / SPLIT) | AGP 升级后,建议使用 MERGED 减少远程请求次数 |
timeout |
远程拉取超时时间 | 升级后输出包更大(如 R8 混淆产物),建议增加超时到 300 秒 |
典型配置示例(settings.gradle.kts):
buildCache {
remote(HttpBuildCache::class.java) {
url = uri("https://buildcache.example.com/cache/")
credentials {
username = "agp_upgrade_user"
password = System.getenv("CACHE_PASSWORD") ?: ""
}
isAllowUntrustedServer = true
compression = "gzip"
storeMode = StoreMode.MERGED
timeout = Duration.ofSeconds(300)
}
}
gradle.properties 中添加 org.gradle.caching.debug=true,可看到每个 Task 从远程缓存拉取时的 HTTP 响应码和时间。如果大量 404,检查缓存 key 是否包含路径。
4.6 本章核心流程与解决方案总览
以下 SVG 流程图总结了多模块项目中缓存同步的主要问题场景、排查步骤与解决方案,可作为日常排障的速查参考。
4.7 常见错误与性能优化建议
以下列表收录了 AGP 升级后缓存同步中最常见的错误及其解决方向:
- 错误:Remote cache returned HTTP 413(Payload Too Large)
解决方案:减小单个缓存的粒度,例如将部分 Task(如mergeDex)设置为不缓存,或启用split存储模式。 - 错误:Cached output from CI fails to decompress on developer machine
解决方案:统一所有环境的 JDK 版本(建议 JDK 17+),并强制使用gzip压缩算法。 - 错误:Multiple modules generate same cache key causing collisions
解决方案:在buildCache.remote中为不同模块设置prefix,例如:modulePath = ":feature-a"作为缓存路径的前缀。 - 性能优化:缓存预热(Cache Warming)
在 CI 中,升级 AGP 后执行gradle assembleRelease --build-cache --parallel --no-daemon生成缓存,后续开发者可直接命中。预热脚本可放在gradle/build-cache-warmup.gradle.kts中管理。 - 稳定性优化:缓存黑名单机制
在gradle.properties中声明不参与缓存的 Task 正则:org.gradle.caching.exclude=.*:mergeDex.*,.*:lint.*,.*:test.*
1.
gradle cleanBuildCache2.
gradle assembleDebug --build-cache --rerun-tasks3. 观察日志中每个 Task 的“Cache hit”统计
4. 若命中率 < 90%,检查模块间
compileSdk 和 AGP 版本一致性5. 若远程缓存频繁 404,检查
buildCache.remote.url 和认证配置
掌握上述缓存同步场景的判断与修复方法,能显著减少 AGP 升级后的构建失败率和回退次数,保证多模块项目的增量构建效率在升级后迅速恢复至正常水平。