Gradle 缓存架构总览:三层缓存结构、构建缓存与任务缓存区别、缓存键生成规则
1.1 三层缓存结构
Gradle 的缓存体系自底向上分为三层,每层负责不同粒度的缓存与失效管理。理解这三层是掌握全流程失效策略的基础。
| 缓存层级 | 存储单位 | 生命周期 | 典型失效原因 |
|---|---|---|---|
| 1. 任务输出缓存(Task Output Cache) | 单个任务的输出文件快照 | 构建会话内 / 跨构建(若启用本地存储) | 输入文件变更、任务依赖变更、Gradle 版本升级 |
| 2. 构建缓存(Build Cache) | 任务输出结果的压缩归档 | 跨构建、跨机器(远程缓存) | 缓存键碰撞、缓存空间满、手动清理 |
| 3. 本地依赖缓存(Dependency Cache) | 解析后的依赖元数据与制品 | 永久(除非手动清除或 Gradle 自动清理) | 仓库元数据过期、依赖版本冲突、resolve 配置变化 |
其中,任务输出缓存和构建缓存是本章讨论的核心。依赖缓存虽然影响输入一致性,但失效逻辑相对独立,将在后续章节单独展开。
1.2 构建缓存 vs. 任务缓存
许多工程师混淆这两个概念,但它们的职责和存储方式完全不同:
- 构建缓存(Build Cache):一种跨项目、跨构建的持久化缓存,存储的是
Task 输出结果的压缩包(tar + 元数据)。可本地或远程部署,常用于 CI/CD 多模块间共享加速。 - 任务缓存(Task Output Cache):指 Gradle 在同一个构建进程内,利用
up-to-date检查机制跳过未变更的任务。此缓存仅在构建会话期间生效,不跨进程保存。
关键区别对照表:
| 维度 | 任务缓存(up-to-date) | 构建缓存(Build Cache) |
|---|---|---|
| 存储介质 | 内存 + .gradle/buildOutputCleanup | 本地文件系统 / HTTP 远程服务器 |
| 有效期 | 构建进程生命周期 | 可跨天、跨机器 |
| 缓存内容 | 不存储输出文件,仅记录输入摘要与上次输出路径 | 完整输出文件归档(tar.gz + 元数据) |
| 失效条件 | 输入文件 hash 变化、任务依赖链变化 | 缓存键不匹配、远程缓存不可用、 |
| 配置开关 | 默认启用,通过 --rerun-tasks 强制关闭 | 需要显式启用 buildCache { local { enabled = true } } |
1.3 缓存键生成规则
无论是任务缓存还是构建缓存,都依赖 缓存键(Cache Key) 来判定输出是否复用。Gradle 的缓存键是一个 SHA-256 哈希值,由以下要素计算得到:
- 任务类名与路径(如
:app:compileJava) - 所有
@Input属性值(字符串、文件路径、布尔值等) - 所有
@InputFiles的文件内容哈希(默认对每个文件计算 64KB 样本哈希) - 所有
@InputDirectory的目录结构快照(文件列表 + 大小 + 最后修改时间) - 任务依赖链的缓存键(递归计算)
- Gradle 版本号及其所使用的插件版本
典型缓存键计算过程可通过以下命令打印观察:
# 启用缓存键调试日志
./gradlew compileJava --info | grep "Cache key for"
# 或查看 build/reports/cache-key/ 下的 JSON 报告
当这些输入中任意一个发生变化,缓存键即失效,Gradle 会重新执行任务。下面用一张示意图概括三层缓存的关系与键生成流程。
实践要点:在多模块项目中,如果一个上游模块的
@OutputDirectory 作为下游模块的 @InputDirectory,下游模块的缓存键会间接包含上游输出哈希。因此,只改变上游代码会触发下游重新执行 —— 这不是错误,而是正确性设计。
1.4 缓存一致性基础
缓存一致性(Cache Consistency)是指在缓存命中时,返回的输出结果必须与重新执行任务完全一致。Gradle 通过以下机制保证:
- 输入确定性:缓存键精确覆盖所有影响输出的输入,包括文件内容、环境变量、任务配置、依赖版本。
- 输出隔离性:任务必须将其所有输出声明在
@OutputDirectory/@OutputFile下,不允许修改外部状态。 - 不可变存储:构建缓存中的归档一旦写入即锁定,不会部分更新。
若想检查当前项目是否满足缓存一致性,可以通过 Gradle 提供的验证任务:
# 执行构建缓存一致性验证(需要安装 gradle-build-consistent 插件或手动校验)
./gradlew cacheConsistencyCheck --info
# 输出示例:
# [ConsistencyCheck] Task :app:compileJava - Cache key d4e5f6... matches local build cache
# [ConsistencyCheck] Task :app:processResources - CACHE MISS (input file changed: src/main/resources/config.yaml)
1.5 常见缓存失效误判场景
| 场景 | 表现 | 根因分析 |
|---|---|---|
| ✗ 任务总是 CACHE MISS | 每次构建都重新编译,即使未改代码 | 任务使用了 @InputFiles 但未排除 build/ 目录,导致输出目录被当作输入 |
| ✗ 远程缓存未命中 | 本地缓存可用,但远程总是 MISS | 远程缓存键计算时包含了本地路径(如 systemProp.user.home) |
| ✗ 任务输出被错误复用 | 修改了源码但缓存返回旧输出 | 未正确声明 @InputFile,或者使用了 @InputDirectory 但只监控了目录名而非内容递归哈希 |
危险操作:在生产构建中,切勿随意设置
cacheIf { true } 不检查任何输入的任务,这会导致缓存中毒(poisoned cache)。务必确保所有输入输出均已声明。
1.6 性能优化第一原则:缓存命中率
缓存系统的核心性能指标是命中率。一项针对大型 Android 项目的统计显示,构建缓存命中率每提升 10%,全量构建时间减少 15%~25%。
提升命中率的基础操作:
- 启用远程构建缓存并在 CI 和开发者之间共享。
- 避免将时间戳、随机数、系统属性纳入缓存键(使用
@Input的normalizeInput或@Ignore)。 - 将可复用的编译任务拆分到独立 Module,减少跨模块依赖引起的连锁失效。
// build.gradle.kts - 精细化缓存键控制示例
tasks.withType<JavaCompile>().configureEach {
// 排除环境变量影响
inputs.property("javaVersion", System.getProperty("java.version"))
.withNormalizer(Normalizer.SimpleNormalizer::class.java)
// 忽略非输入变更
outputs.cacheIf { true }
}
实操验证:完成本章学习后,请运行以下命令并记录缓存命中次数:
你将看到每个任务对应的状态:
./gradlew clean assemble --info 2>&1 | grep -E "CACHE|(up-to-date)"你将看到每个任务对应的状态:
FROM-CACHE(远程缓存命中)、UP-TO-DATE(本地任务缓存)、EXECUTING(重新执行)。
1.7 进入后续章节前的关键概念清单
- ☐ 三层缓存结构及各自存储单位
- ☐ 构建缓存 vs 任务缓存的启用区别
- ☐ 缓存键由 6 类要素计算得出
- ☐ 缓存一致性检查方法(命令与日志解读)
- ☐ 三大典型失效误判场景的修复策略