3. Gradle 缓存机制原理:构建缓存、配置缓存与本地缓存目录结构

在多模块项目中,每次 AGP 版本升级都会触发大量任务重新执行、配置重新解析,导致构建时间倍增。理解 Gradle 的三层缓存体系——构建缓存(Build Cache)、配置缓存(Configuration Cache)以及本地缓存目录结构——是解决版本升级后缓存同步与冲突问题的前提。本章从底层机制到实用配置,逐层剖析每个缓存的作用域、失效场景和调试方法。

概念速览:构建缓存缓存任务输出,配置缓存缓存构建配置阶段的结果,本地缓存目录负责存储下载的依赖、生成的中间文件。三者相互独立又共同影响构建性能。

3.1 构建缓存(Build Cache)

3.1.1 原理与作用域

构建缓存将每个任务的输入(inputs)哈希后,缓存任务的输出文件。当输入不变时,后续构建可直接从缓存拉取输出,跳过任务执行。在 AGP 升级场景下,如果模块 A 的编译任务输入(源码、AGP API 版本、依赖坐标)未变,即使 AGP 版本号变化(但 API 行为兼容),缓存仍可命中。

3.1.2 配置方式

// settings.gradle.kts
buildCache {
    local(DirectoryBuildCache::class) {
        directory = File(rootDir, ".build-cache")
        removeUnusedEntriesAfterDays = 7
    }
    remote(HttpBuildCache::class) {
        url = URI("https://cache.example.com/")
        isPush = true
        credentials {
            username = "cache-user"
            password = "cache-pass"
        }
    }
}

3.1.3 调试命令

# 查看当前缓存的命中统计
./gradlew build --build-cache --info | grep "Build cache"
# 强制清空本地构建缓存
./gradlew cleanBuildCache
版本升级冲突点:AGP 版本变化会导致某些任务的@CacheableTask注解检测到输出类型变化(如 AAPT2 生成的资源二进制格式不同),从而默认不命中。此时需检查 taskOutputs.upToDateWhen 是否合理,或考虑使用 cacheIf 自定义缓存条件。

3.2 配置缓存(Configuration Cache)

3.2.1 原理与生命周期

配置缓存将 settings.gradle.kts 和 build.gradle.kts 的配置阶段脚本执行结果序列化为二进制缓存,后续构建如果检测到 Gradle 脚本、环境变量、系统属性未变,则直接反序列化配置,跳过配置阶段。在 AGP 升级后,如果 apply plugin: "com.android.application" 或依赖仓库地址未变,但 AGP 版本号变了,默认不会触发配置缓存失效,因为配置缓存的 key 不包含构建脚本中的版本号(除非显式声明了输入属性)。

3.2.2 启用与验证

# 启用配置缓存(gradle.properties 或命令行)
org.gradle.configuration-cache=true

# 首次构建写入缓存,第二次验证是否重用
./gradlew assembleDebug --configuration-cache

# 查看缓存失效原因
./gradlew assembleDebug --configuration-cache --info | grep "configuration-cache"

3.2.3 常见错误及修复

错误信息原因解决方法
Gradle could not serialize...配置缓存不支持某些闭包或匿名类改为使用一等函数或 Provider 接口
Configuration cache state invalidated脚本中引用了非确定性值(如 System.currentTimeMillis())使用 providers.systemProperty() 代替
Cached configuration is staleAGP 升级后某些 task 的 input 属性在配置阶段未注册检查 task.inputs 声明是否完整
性能陷阱:配置缓存与 AGP 的 buildConfig 字段结合时,如果 BuildConfig 依赖 project.property 而未在 input 中显式声明,缓存可能产生错误的 BuildConfig 值。务必使用 BasePluginExtension.buildConfigField 并确保所有变量通过 providers 注入。

3.3 本地缓存目录结构

3.3.1 目录全景

Gradle 默认在 $HOME/.gradle/caches/ 下存储所有本地缓存,结构如下:

$HOME/.gradle/caches/
├── journal-1/               # 缓存元数据日志
├── modules-2/               # 解析后的模块元数据(POM、Ivy)
├── transforms-3/            # 资源/文件转换缓存(如 AAPT2)
├── jars-3/                  # 已下载的 JAR/AAR
├── kotlin-dsl/              # Kotlin DSL 编译缓存
├── build-cache-1/           # 构建缓存本地存储(如果开启)
├── configuration-cache/     # 配置缓存序列化文件
│   ├── 1/                   # 按 Gradle 版本分目录
│   └── ...
└── gradle-wrapper/          # Gradle Wrapper 下载的发行版

3.3.2 版本升级后的冲突点

  • AGP 版本升级后,transforms-3/ 中的缓存可能因 AAPT/资源编译器版本变更而失效,建议在升级后执行 ./gradlew clean 清除 transforms 缓存。
  • modules-2/ 中缓存的依赖元数据如果包含旧的 Gradle Module Metadata(GMM),可能导致新 AGP 解析失败。可执行 ./gradlew --refresh-dependencies 强制刷新。
  • configuration-cache/ 目录按 Gradle 版本号存放,升级 AGP 后如果 Gradle 版本未变,旧配置缓存可能残留,需手动删除或使用 --no-configuration-cache 重新生成。

3.3.3 清理策略

# 仅清理构建缓存
./gradlew cleanBuildCache
# 清理所有本地缓存(注意:会删除所有依赖和转换缓存)
rm -rf $HOME/.gradle/caches/ && mkdir -p $HOME/.gradle/caches/
# 安全删除指定版本缓存(保留已下载的 jar 包)
rm -rf $HOME/.gradle/caches/transforms-3/
多模块项目建议:在 CI 上每次 AGP 升级后,执行一次rm -rf $HOME/.gradle/caches/transforms-3/ $HOME/.gradle/caches/configuration-cache/,可减少 80% 的缓存冲突导致的构建失败。

3.4 三种缓存的协作关系

下图展示了在多模块 AGP 升级过程中,三个缓存如何串联:

Gradle 三层缓存协作流程(AGP 升级后) 配置脚本解析 配置缓存 任务执行 构建缓存 本地缓存目录 版本升级失效点 AGP 版本号变化可能导致 配置缓存 & 构建缓存双失效 解决方案 升级后清理 transforms 缓存 并重新生成配置缓存

3.5 性能与稳定性优化

  1. 为构建缓存配置合适的远程 URL:多模块项目中,使用同一台 CI 机器时,远程构建缓存可以跨构建共享已编译的 AAR,减少重复任务。
  2. 配置缓存开启前清理序列化风险:在 AGP 升级后,先执行 --no-configuration-cache 正常构建一次,确保所有 task 配置正常,再启用配置缓存。
  3. 本地缓存目录与项目分离:在 gradle.properties 中设置 org.gradle.caches.dir=./.gradle-caches 将缓存放在项目目录内,便于 CI 流程整体清理。
  4. 利用 --rerun-tasks 验证一致性:AGP 升级后,可先执行 ./gradlew assembleDebug --rerun-tasks --build-cache 强制所有任务重跑,然后将生成的构建缓存推送至远程,确保缓存与新版 AGP 一致。