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