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