本地构建缓存:配置、调优与冲突处理
本地构建缓存是 Gradle 加速增量构建最直接的手段。与编译守护进程(Daemon)的运行时缓存不同,本地构建缓存将任务输出序列化到磁盘,使得即使清空 build/ 目录,或者在同一台机器上切换分支、切换项目副本,都能复用之前的构建结果。本章深入讲解缓存目录结构、容量控制、失效策略,并通过 --build-cache 标志触发缓存,同时详解缓存冲突的产生与解决路径。
4.1 缓存目录结构与配置
Gradle 使用 ~/.gradle/caches/build-cache-1 作为本地构建缓存的默认根目录。该目录下以任务缓存键的 SHA-256 值命名子目录,每个子目录存储一个任务输出的快照(包含 output/ 和元数据 .ser 文件)。
- 默认位置:
%USER_HOME%/.gradle/caches/build-cache-1(Windows/macOS/Linux 一致)。 - 自定义目录:通过
settings.gradle.kts的buildCache块指定。 - 项目级缓存:也可将缓存目录置于项目根目录,便于清理或 CI 场景。
// settings.gradle.kts —— 配置本地构建缓存目录与容量
buildCache {
local {
// 自定义缓存存储路径(建议使用路径)
directory = File(rootProject.projectDir, ".gradle-build-cache")
// 设置最大缓存大小为 2 GB
maxCacheSize = 2L * 1024 * 1024 * 1024
// 移除缓存条目中最旧的 10%
removeUnusedEntriesAfterDays = 7
}
}
实践建议:在 CI 环境中,将
directory 设置为共享网络存储或持久化卷,可以实现跨构建的缓存复用;但需注意读写延迟对构建总时间的影响。本地开发建议保持默认路径以利用 SSD 速度。
4.2 最大缓存大小与失效策略
Gradle 本地缓存的失效策略基于 LRU(最近最少使用)和 TTL(生存时间)的组合:
| 参数 | 配置方式 | 默认值 | 说明 |
|---|---|---|---|
maxCacheSize | local { maxCacheSize = … } | 无限制(单位为字节) | 缓存目录可占用的最大磁盘空间 |
removeUnusedEntriesAfterDays | local { removeUnusedEntriesAfterDays = … } | 7 天 | 条目未被访问超过指定天数即视为过期 |
| 缓存清理触发时机 | Gradle 在每次构建结束时检查 | — | 超过 maxCacheSize 时按 LRU 删除;过期条目先被清理 |
| 强制清理 | 手动执行 --cache-clean(仅 CI 插件) | — | 生产环境建议通过 cleanBuildCache 任务显式控制 |
失效策略执行顺序:首先清除超过 removeUnusedEntriesAfterDays 天未使用的条目;如果缓存仍旧超过 maxCacheSize,继续按 LRU 删除直到低于限制值的 90%。
// 显式清理本地构建缓存的任务(在 build.gradle.kts 中注册)
tasks.register("cleanBuildCache") {
doLast {
val cacheDir = layout.cacheDirectory.asFile
cacheDir.deleteRecursively()
println("本地构建缓存已清除:${cacheDir.absolutePath}")
}
}
4.3 启用缓存:--build-cache 标志
Gradle 默认不使用构建缓存(即使配置了本地存储),需要显式传入 --build-cache 或开启全局缓存选项。
# 命令行启用构建缓存(推荐方式)
./gradlew assemble --build-cache
# 或在 gradle.properties 中设置为全局默认
org.gradle.caching=true
- 单次启用:适合调试或对比性能,避免全局开启影响未准备好的项目。
- 全局启用:适用于所有 project 都兼容构建缓存的项目。
- 仅本地缓存:不加
remote配置时,缓存数据只存储在本机目录。
重要注意事项:开启
--build-cache 后,如果某些自定义任务没有正确声明 @Input/@Output,可能会导致结果错乱。第一次使用缓存时,建议并行运行 --build-cache 和 --no-build-cache 各一次,比对输出内容的一致性。
4.4 处理缓存冲突(Cache Conflict)
缓存冲突发生在两个不同的任务输出被映射到相同的缓存键时。典型原因包括:
- 非确定性输入:任务中使用
System.currentTimeMillis()、UUID 或外部无序文件列表作为@InputFiles。 - 相对路径与路径混淆:同一个文件通过不同路径引用。
- 多 Gradle 版本叠加:不同 Gradle 版本对任务类型输出的序列化格式不兼容。
- 缓存键碰撞:极低概率的 SHA-256 冲突,通常由不规范的
normalization配置引起。
Gradle 在发现冲突时会引发 BuildCacheController$CacheConflictException,并自动回退至重新执行任务。解决方案分为三个层面:
| 层面 | 操作 | 配置/命令示例 |
|---|---|---|
| 任务定义 | 确保所有影响输出的因素都被声明为 @Input,并禁用不可重现的代码路径。 |
|
| 缓存键自定义 | 通过 BuildCacheKeyBuilder 在任务中追加额外的区分符。 |
|
| 本地缓存策略 | 设置 directory 隔离不同项目/分支的缓存目录。 |
|
// 调试缓存冲突:开启缓存日志,查看哪个任务命中冲突
// 在 gradle.properties 中开启调试
org.gradle.caching.debug=true
// 构建时观察输出
./gradlew compileJava --info | grep -i "cache conflict"
4.5 常见错误与性能优化清单
常见错误场景:
- “Caching disabled for task X: not cacheable”:任务未声明
@CacheableTask或存在@OutputDirectory未正确声明。 - “Cache entry was evicted before reuse”:最大缓存过小或
removeUnusedEntriesAfterDays值过短,频繁写入导致旧条目被过早清理。 - “Build cache is not enabled”:仅配置了
buildCache { local { … } }但未在命令行加--build-cache。
性能优化要点:
- 将
maxCacheSize设置为项目输出大小的 3~5 倍,避免频繁 LRU 删除。 - 对 CI 环境,统一 Gradle 版本与 JDK 版本,减少缓存键变化几率。
- 使用
--build-cache --parallel并发执行可进一步提高缓存命中后的构建效率。 - 通过
gradle build --profile生成报告,分析cache ratio和avoided execution指标。
下表总结了本地缓存配置的关键权衡:
| 配置项 | 推荐值 | 过高/过低的影响 |
|---|---|---|
maxCacheSize | 2 GB ~ 5 GB | 过低:频繁 eviction;过高:磁盘压力大、扫描时间长 |
removeUnusedEntriesAfterDays | 7 天 | 过长:老旧条目占用空间;过短:有效缓存被误清除 |
| 缓存目录位置 | SSD 本地路径 | NFS/网络路径:IO 延迟会抵消缓存加速效果 |