第2章 Gradle 缓存机制深度剖析:任务输出缓存、构建缓存、本地缓存与远程缓存的差异、缓存键计算规则、快照和输入文件依赖
本章从缓存体系出发,逐层拆解 Gradle 四种缓存类型的定位、存储方式与生命周期;深入缓存键的 SHA-256 计算过程、输入快照的生成规则,以及如何通过输入文件依赖控制缓存命中。内容覆盖原理、配置、调试与常见失误,是后续章节优化缓存命中率的理论基础。
2.1 缓存体系概览
Gradle 4.3 之后引入的构建缓存(Build Cache)由任务输出缓存(Task Output Cache)上层封装而成,本地实现与远程实现共享同一套键规则。下表对比四类缓存的核心差异:
| 缓存类型 | 存储范围 | 存储介质 | 生命周期 | 键计算基准 | 典型场景 |
|---|---|---|---|---|---|
| 任务输出缓存 | 单个任务 | 内存 + 本地文件 | 构建进程内 | 任务输入 + 快照 | 增量编译 |
| 构建缓存 | 整个构建 | 压缩 tar 包 | 跨构建(可持久化) | 同上,包装为 tar | 本地多次构建复用 |
| 本地缓存 | 单机 | ~/.gradle/caches/build-cache-1/ | 按 LRU 策略淘汰 | 任务输出缓存键 | 开发者日常构建 |
| 远程缓存 | 团队 / CI | HTTP 服务(S3/Artifactory) | 服务端策略 | 同上 + 远程 URL 哈希 | CI/CD 共享加速 |
2.2 任务输出缓存
任务输出缓存是最底层、粒度最小的缓存单元。当 Gradle 确定一个任务的输入(Inputs)与前一次执行完全一致时,直接复用上一次任务产生的输出文件(Outputs),不再执行该任务。
启用方式
在自定义任务上添加 @CacheableTask 注解,并确保 @Input、@InputFiles、@OutputDirectory 等注解正确标记。
@CacheableTask
abstract class MyTransformTask extends DefaultTask {
@InputFile
abstract RegularFileProperty getInputFile()
@OutputDirectory
abstract DirectoryProperty getOutputDir()
@TaskAction
void transform() {
// 实际转换逻辑
}
}
缓存键计算规则
缓存键(Cache Key)由以下元素拼接后经 SHA-256 哈希得到:
- 任务类型的完全限定类名(含类加载器)。
- 所有
@Input/@InputFiles注解声明的参数值。 - 输入文件的 内容哈希(非修改时间),按文件在输入集合中的相对路径顺序排列。
- 任务 action 自身无法直接注入,但通过类名和 Gradle 版本间接隐含。
@InputFiles 使用了 getAbsolutePath() 或依赖了环境变量,不同机器上同一份代码会产生不同的缓存键,导致 100% 未命中。2.3 构建缓存(本地与远程)
构建缓存将任务输出缓存打包为 .tar 压缩包,并存入本地目录或远程存储。当 Gradle 需要某个任务的输出时,先用键查找本地缓存;不存在则请求远程缓存;远程缓存也未命中时才会重新执行任务。
本地缓存位置
~/.gradle/caches/build-cache-1/
远程缓存配置
在 settings.gradle.kts 或 settings.gradle 中声明:
// settings.gradle.kts
buildCache {
local {
isEnabled = true
removeUnusedEntriesAfterDays = 30
}
remote {
url = uri("https://cache.mycompany.com/cache/")
isPush = true
isEnabled = true
credentials {
username = "gradle"
password = System.getenv("CACHE_PASS")
}
}
}
调试命令
gradle build --build-cache --info # 查看缓存命中/未命中详情
gradle build --cache-miss-tracking # 生成未命中分析报告
2.4 快照(Snapshot)和输入文件依赖
输入文件的快照并非使用文件最后修改时间(mtime),而是通过 内容寻址 方式计算每个文件的哈希值(默认为 SHA-256)。Gradle 在解析输入时会执行以下步骤:
- 遍历输入文件集合(
@InputFiles、@Classpath等)。 - 对每个文件按相对路径(相对于输入根目录)计算内容哈希。
- 将所有文件哈希与路径列表序列化为规范化的快照字符串。
- 将该快照字符串作为缓存键的一部分输入 SHA-256 计算。
输入文件依赖:如果任务 A 的 @OutputDirectory 被任务 B 的 @InputFiles 引用,那么任务 B 的缓存键会包含任务 A 的输出快照。这种依赖关系使缓存失效自动向下游传播。
- 文件时间戳改变但内容不变。
- 文件系统重命名或移动(路径会变,注意路径归一化)。
- 在构建机器和目标机器之间拷贝文件。
2.5 缓存键计算深度解析
Gradle 内部使用 org.gradle.caching.internal.origin.OriginMetadata 记录构建源信息。键计算的完整公式可视为:
CacheKey = SHA-256(
taskClass.fqn + ":" +
gradleVersion + ":" +
serialize(inputs) + // 所有 @Input 注解属性值
serialize(inputFilesSnapshots) // 内容哈希列表
)
常见导致键不同的原因:
| 影响因素 | 示例 | 解决方案 |
|---|---|---|
| 路径 | @InputFile 使用 getAbsolutePath() | 使用 @Classpath 或相对路径 |
| 环境变量 | 依赖 System.getenv("HOME") | 通过 @Internal 或 Provider 包装 |
| 时间戳信息 | 在任务 action 中使用 System.currentTimeMillis() | 注解为 @Internal |
| 类路径版本 | 使用未 pinned 的 plugin 版本 | 固定版本或使用 Gradle 版本目录 |
| 文件编码/换行 | Linux 与 Windows 换行差异 | 在 Git 中配置 .gitattributes 统一换行 |
2.6 调试与诊断
当缓存命中率异常时,可通过以下手段进行诊断:
- 构建扫描(Build Scan):
--scan参数会在gradle.com生成一份包含缓存命中/未命中详情、任务输入变化归因的可视化报告。 --cache-miss-tracking:Gradle 7.5+ 支持,在项目根目录生成build/cache-miss-tracker报告,列出每个未命中任务的具体差异。- 打印缓存键:通过
gradle --build-cache --info --cache-miss-tracking并查看日志中的Cache key for task ':xxx' is XXXXX。
# 以纯文本形式查看任务输入快照
gradle help --task :compileJava --info
2.7 常见错误与解决方案
fileMode 包含在 hash 中,导致不同操作系统下同一个文件的快照不同,远程缓存几乎从未命中。解决方法:在 build.gradle.kts 中配置输入归一化。// 归一化配置示例
normalization {
runtimeClasspath {
ignoreFileName = true
ignoreClasspathEntryMetaInf = false
}
properties("**/version.properties") {
// 忽略特定属性文件中的 build 时间戳
ignoreProperty("buildTime")
}
}
其他常见错误:
- 远程缓存超时:在大文件场景下,HTTP 连接超时可设置
.cleanupInterval和connectionTimeout。 - 缓存空间撑爆:本地缓存默认无上限,建议设置
removeUnusedEntriesAfterDays = 30。 - 权限拒绝:远程缓存服务需要认证,务必通过 CI 环境变量注入,避免硬编码。
2.8 性能与稳定性优化
- 减少键的噪声:将非关键输入(如临时文件、日志级别)标识为
@Internal,避免不必要的变化。 - 使用
@SkipWhenEmpty:当输入文件集合为空时跳过任务并直接复用上次输出。 - 调整远程缓存并发:在
gradle.properties中设置org.gradle.caching.http.maxConnections=8提高 I/O 并发。 - 结合 Git 哈希:在 CI 上,使用 Git commit hash 作为构建的前置标识,配合
--build-cache在相同代码变更时最大化复用。 - 定期清理本地缓存:通过
gradle cleanBuildCache或脚本定时删除过期条目。