本地构建缓存:配置、调优与冲突处理

本地构建缓存是 Gradle 加速增量构建最直接的手段。与编译守护进程(Daemon)的运行时缓存不同,本地构建缓存将任务输出序列化到磁盘,使得即使清空 build/ 目录,或者在同一台机器上切换分支、切换项目副本,都能复用之前的构建结果。本章深入讲解缓存目录结构、容量控制、失效策略,并通过 --build-cache 标志触发缓存,同时详解缓存冲突的产生与解决路径。

本地构建缓存工作流程与配置要素 本地构建缓存:配置决策点与失效流程 任务执行请求 计算 缓存键(inputs+task) 查询本地缓存目录 ${cacheDir}/cache/ 命中 未命中 检查失效策略 TTL / 最大缓存大小 / 冲突 有效 重用缓存输出 失效/满 重新执行 并更新缓存 未命中 → 执行并写入 冲突检测:缓存键 + 内容哈希 默认缓存目录 ~/.gradle/caches/ 可配置为项目本地或共享位置

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(生存时间)的组合:

参数配置方式默认值说明
maxCacheSizelocal { maxCacheSize = … }无限制(单位为字节)缓存目录可占用的最大磁盘空间
removeUnusedEntriesAfterDayslocal { 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)

缓存冲突发生在两个不同的任务输出被映射到相同的缓存键时。典型原因包括:

  1. 非确定性输入:任务中使用 System.currentTimeMillis()、UUID 或外部无序文件列表作为 @InputFiles。
  2. 相对路径与路径混淆:同一个文件通过不同路径引用。
  3. 多 Gradle 版本叠加:不同 Gradle 版本对任务类型输出的序列化格式不兼容。
  4. 缓存键碰撞:极低概率的 SHA-256 冲突,通常由不规范的 normalization 配置引起。

Gradle 在发现冲突时会引发 BuildCacheController$CacheConflictException,并自动回退至重新执行任务。解决方案分为三个层面:

层面操作配置/命令示例
任务定义确保所有影响输出的因素都被声明为 @Input,并禁用不可重现的代码路径。
@InputFiles
val inputFiles: FileCollection = …
缓存键自定义通过 BuildCacheKeyBuilder 在任务中追加额外的区分符。
@Override
public void appendToCacheKey(BuildCacheKeyBuilder builder) {
    builder.appendString(getGradleVersion());
}
本地缓存策略设置 directory 隔离不同项目/分支的缓存目录。
local { directory = file("…/${project.name}") }
// 调试缓存冲突:开启缓存日志,查看哪个任务命中冲突
// 在 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 指标。

下表总结了本地缓存配置的关键权衡:

配置项推荐值过高/过低的影响
maxCacheSize2 GB ~ 5 GB过低:频繁 eviction;过高:磁盘压力大、扫描时间长
removeUnusedEntriesAfterDays7 天过长:老旧条目占用空间;过短:有效缓存被误清除
缓存目录位置SSD 本地路径NFS/网络路径:IO 延迟会抵消缓存加速效果