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