第一章 构建缓存核心原理
1.1 构建缓存作用机制
Gradle 构建缓存通过重用先前构建的输出(例如 .class 文件、JAR、测试报告)来避免重复执行相同的任务。其核心是 任务输出缓存:当任务输入与历史构建完全一致时,Gradle 直接复用缓存中的输出,跳过任务执行。
下图展示了缓存从输入到命中的完整生命周期:
流程解读:任务输入(源文件路径与内容、依赖坐标、Gradle 版本、操作系统)经过统一散列生成缓存键;该键作为唯一标识在键/值数据库中查询;若命中,则直接返回输出工件;若未命中,任务正常执行并将输出存入缓存。
1.2 缓存作用域:本地与远程
作用域决定了缓存数据在哪个范围内共享。实现层面分为两级:
| 特性 | 本地缓存 (Local) | 远程缓存 (Remote) |
|---|---|---|
| 存储位置 | ~/.gradle/caches/build-cache-1/ |
HTTP 服务器 (如 S3、Artifactory) |
| 网络依赖 | 无 | 需要 HTTP(S) 连接 |
| 共享范围 | 单机 | 团队 / CI 所有节点 |
| 磁盘空间 | 由 Gradle 自动清理 (LRU) | 服务器端策略控制 |
| 缓存键校验 | 文件系统哈希 | HTTP ETag / Content‑Hash |
作用域配置入口:在
settings.gradle.kts 中启用并定义作用域范围。
// settings.gradle.kts
buildCache {
local {
enabled = true
directory = File(System.getProperty("user.home"), ".gradle/caches/build-cache-1")
}
remote {
url = uri("https://build-cache.mycompany.com/cache/")
isPush = true
credentials {
username = "cache-user"
password = System.getenv("CACHE_") ?: ""
}
}
}
关键提醒:远程缓存需要显式声明
isPush = true 才会上传缓存条目;CI 环境下务必通过环境变量注入密码,避免将凭证硬编码在版本控制中。
1.3 缓存键的精确组成
缓存键是一个长度固定的十六进制字符串(SHA-256),由以下因素共同决定:
- 任务类型标识符 —— 如
com.example.MyTask,确保不同类型的任务不会相互污染。 - 输入文件路径及内容哈希 —— 每个声明为
@InputFiles或@InputFile的文件都会被加入计算。 - 输入属性的值 —— 标注了
@Input的字符串、整数等。 - Gradle 版本与操作系统名称 —— 防止跨平台二进制不兼容。
- 输出文件路径的通配结构 —— 确保输出位置不会意外改变键值。
Gradle 内部通过 HashCode 类将上述元素拼接为唯一的 32 字节摘要。任何一项发生变化都会导致新键生成,从而触发缓存未命中。
1.4 缓存命中条件
命中条件遵循严格的全等比较原则:
- 键精确匹配:本地或远程存储中存在与当前生成键完全相同的条目。
- 输出结构可用:缓存中的输出目录结构与任务声明的
@OutputDirectory/@OutputFile一致。 - 未达到生存期 (TTL):远程缓存条目未过期(可通过 HTTP Cache‑Control 头控制)。
调试命中状态:使用
--info 或 --debug 日志级别运行构建,Gradle 会输出类似 Task :compileJava: cache hit 或 cache miss for key: 2a3f8c... 的日志行。这是排查缓存未命中最直接的手段。
# 启用缓存并输出详细日志
gradle build --build-cache --info
1.5 缓存存储结构与回收
本地缓存采用分层文件系统存储,按键的前缀分目录保存:
~/.gradle/caches/build-cache-1/
├── 2a/
│ └── 3f/
│ └── 8c... (缓存工件)
├── ab/
│ └── cd/
│ └── ef...
└── gc.properties # 回收策略配置
每个缓存条目包含两部分:
- 元数据文件 —— 存储键、创建时间、任务类型、输入指纹。
- 输出快照 —— 任务输出的压缩归档(tar/zst 格式)。
Gradle 在构建完成或启动时会检查磁盘占用,若超过 MaxCacheSize(默认 10 GB)则按最近最少使用 (LRU) 策略清理旧条目。
性能陷阱:超大单个缓存文件(如包含全部依赖的 fat JAR)会显著降低缓存上传/下载速度,并容易撑满本地存储。在
build.gradle.kts 中合理拆分任务输出,避免将不必要的文件纳入缓存。