Skip to content

构建缓存深度解析

源:Gradle 官方文档 - Build Cache

构建缓存通过保存任务输出并在输入相同时复用,实现跨机器、跨分支的构建加速。

构建缓存概念

什么是构建缓存

构建缓存 vs UP-TO-DATE

特性UP-TO-DATE构建缓存
范围本地 build 目录本地 + 远程
跨分支
跨机器
团队共享

示例场景

bash
# 分支 A 构建 git checkout feature-a ./gradlew build # 第一次完整构建  # 切换到分支 B git checkout feature-b ./gradlew build # 没有缓存,重新构建  # 切回分支 A git checkout feature-a ./gradlew build # UP-TO-DATE(本地有 build 目录)

有构建缓存

bash
git checkout feature-a ./gradlew build # 第一次,写入缓存  git checkout feature-b ./gradlew build # 从缓存读取 feature-a 的产物!  git checkout feature-c ./gradlew build # 继续从缓存读取

启用构建缓存

本地缓存

gradle.properties

properties
org.gradle.caching=true

或命令行

bash
./gradlew build --build-cache

缓存位置

~/.gradle/caches/build-cache-1/

settings.gradle.kts 配置

kotlin
buildCache {  local {  isEnabled = true  directory = file("${rootDir}/.gradle/build-cache")  removeUnusedEntriesAfterDays = 7  } }

缓存键计算

什么决定缓存键

Gradle 为每个可缓存任务计算唯一的缓存键(cache key):

输入(Inputs)

  • @Input 注解的值
  • @InputFile 文件内容的 hash
  • @InputDirectory 目录内所有文件的 hash

任务实现

  • 任务类的字节码 hash
  • 任务依赖的库的 hash

任务路径

  • 任务的完整路径(如 :app:compileDebugKotlin

缓存键计算示例

kotlin
@CacheableTask abstract class TransformTask : DefaultTask() {  @get:InputFile  @get:PathSensitive(PathSensitivity.RELATIVE)  abstract val inputFile: RegularFileProperty    @get:Input  abstract val option: Property<String>    @get:OutputFile  abstract val outputFile: RegularFileProperty }

缓存键包含

  • inputFile 的内容 SHA-256
  • option 的值
  • TransformTask 类的字节码 hash
  • 任务路径

缓存失效原因

常见导致缓存失效的问题

使用时间戳

kotlin
// ❌ 错误:每次时间戳都不同 @get:Input val timestamp = System.currentTimeMillis()

使用绝对路径

kotlin
// ❌ 错误:不同机器路径不同 @get:InputFile val file = RegularFileProperty().apply {  set(File("/Users/virogu/project/input.txt")) }

解决方案

kotlin
// ✅ 正确:使用相对路径 @get:InputFile @get:PathSensitive(PathSensitivity.RELATIVE) abstract val inputFile: RegularFileProperty

未声明的输入

kotlin
// ❌ 错误:读取文件但未声明 @TaskAction fun execute() {  val config = File("config.txt").readText()  // 使用 config }

解决方案

kotlin
@get:InputFile abstract val configFile: RegularFileProperty  @TaskAction fun execute() {  val config = configFile.get().asFile.readText() }

远程缓存

HTTP 远程缓存

settings.gradle.kts

kotlin
buildCache {  local {  isEnabled = true  }  remote<HttpBuildCache> {  url = uri("https://gradle-cache.example.com/cache/")  isPush = true // 允许推送到远程    credentials {  username = providers.environmentVariable("CACHE_USERNAME").orNull  password = providers.environmentVariable("CACHE_PASSWORD").orNull  }  } }

仅读取模式

CI 环境推送,开发者仅读取

kotlin
buildCache {  remote<HttpBuildCache> {  url = uri("https://gradle-cache.example.com/cache/")    // 仅 CI 环境推送  isPush = providers.environmentVariable("CI")  .map { it.toBoolean() }  .getOrElse(false)  } }

远程缓存服务器

搭建简单的缓存服务器(使用 Gradle Build Cache Node):

bash
# Docker 运行 docker run -d \  -p 8080:8080 \  -v /var/gradle-cache:/data \  gradle/build-cache-node:latest

配置

kotlin
buildCache {  remote<HttpBuildCache> {  url = uri("http://localhost:8080/cache/")  isPush = true  } }

缓存诊断

查看缓存命中情况

bash
./gradlew build --build-cache --info

查找

Build cache key for task ':app:compileDebugKotlin' is abc123... Task ':app:compileDebugKotlin' is not up-to-date because:  No history is available. Stored cache entry for task ':app:compileDebugKotlin' with cache key abc123...

分析缓存未命中

bash
./gradlew build --build-cache --info | grep "cache key"

输出示例

Build cache key for task ':app:processDebugResources' is def456... Loaded cache entry for task ':app:processDebugResources' with cache key def456... FROM-CACHE

Build Scan 查看缓存

bash
./gradlew build --build-cache --scan

报告内容

  • 缓存命中率
  • 哪些任务从缓存加载
  • 哪些任务写入缓存
  • 缓存大小

编写可缓存任务

@CacheableTask 注解

kotlin
@CacheableTask abstract class MyTask : DefaultTask() {  @get:InputFile  @get:PathSensitive(PathSensitivity.RELATIVE)  abstract val inputFile: RegularFileProperty    @get:OutputFile  abstract val outputFile: RegularFileProperty    @TaskAction  fun execute() {  // 任务逻辑  } }

PathSensitive 策略

选择正确的路径敏感性

kotlin
// 源代码:相对路径 @get:InputFiles @get:PathSensitive(PathSensitivity.RELATIVE) abstract val sources: ConfigurableFileCollection  // 配置文件:仅文件名 @get:InputFile @get:PathSensitive(PathSensitivity.NAME_ONLY) abstract val config: RegularFileProperty  // 资源文件:忽略路径 @get:InputFiles @get:PathSensitive(PathSensitivity.NONE) abstract val images: ConfigurableFileCollection

规范化输入

kotlin
@get:Input @get:Optional abstract val options: MapProperty<String, String>  @TaskAction fun execute() {  // 确保顺序一致  val sorted = options.get().toSortedMap()  // 使用 sorted }

实战案例

案例1:Android 项目缓存优化

gradle.properties

properties
org.gradle.caching=true org.gradle.parallel=true org.gradle.vfs.watch=true  android.enableAdditionalTestOutput=false android.nonTransitiveRClass=true

settings.gradle.kts

kotlin
buildCache {  local {  isEnabled = true  removeUnusedEntriesAfterDays = 30  } }

案例2:CI/CD 缓存策略

GitHub Actions

yaml
name: Build  on: [push, pull_request]  jobs:  build:  runs-on: ubuntu-latest  steps:  - uses: actions/checkout@v3    - name: Setup Gradle  uses: gradle/gradle-build-action@v2  with:  cache-read-only: ${{ github.ref != 'refs/heads/main' }}    - name: Build  run: ./gradlew build --build-cache

案例3:团队共享缓存

搭建缓存服务器

bash
# 使用 Gradle Enterprise 或自建 docker run -d \  -p 5071:5071 \  -v /data/build-cache:/data \  gradle/build-cache-node:latest

配置

kotlin
buildCache {  remote<HttpBuildCache> {  url = uri("http://cache-server.company.com:5071/cache/")    // 仅 CI 推送  isPush = System.getenv("CI") == "true"    credentials {  username = System.getenv("CACHE_USER")  password = System.getenv("CACHE_PASS")  }  } }

缓存性能优化

清理旧缓存

bash
# 清理本地缓存 ./gradlew cleanBuildCache  # 或手动删除 rm -rf ~/.gradle/caches/build-cache-1

配置缓存大小

kotlin
buildCache {  local {  // 限制缓存大小  maxSize = gradle.gradleUserHomeDir  .resolve("caches/build-cache-1")  .apply { mkdirs() }  .usableSpace / 10 // 使用磁盘 10% 空间  } }

监控缓存效率

使用 Build Scan

bash
./gradlew build --build-cache --scan

关注指标

  • Cache Hit Rate(缓存命中率)
  • Cacheable Tasks(可缓存任务数)
  • Avoided Task Execution Time(节省的时间)

最佳实践

启用缓存

properties
org.gradle.caching=true org.gradle.parallel=true

使用相对路径

kotlin
@get:PathSensitive(PathSensitivity.RELATIVE)

避免绝对路径和时间戳

  • 不使用 System.currentTimeMillis()
  • 不使用 File("/absolute/path")

CI 策略

  • CI 推送缓存
  • 开发者仅读取
  • 定期清理旧缓存

监控和调试

  • 使用 Build Scan
  • 检查缓存命中率
  • 优化缓存键

自定义任务

  • 添加 @CacheableTask
  • 正确声明输入输出
  • 使用 PathSensitive