构建缓存深度解析
构建缓存通过保存任务输出并在输入相同时复用,实现跨机器、跨分支的构建加速。
构建缓存概念
什么是构建缓存
构建缓存 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-CACHEBuild 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=truesettings.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