配置缓存:配置缓存原理、兼容性检查、常见问题
配置缓存(Configuration Cache)是 Gradle 6.6 引入的一个重磅特性。说白了,它就是把构建脚本的配置阶段结果缓存下来,下次构建直接复用,跳过重复的配置计算。
我刚开始接触这个特性时,心里也犯嘀咕:这玩意儿靠谱吗?会不会跟我的插件打架?后来在几个中型项目里试了试,效果确实不错。今天我就把配置缓存的核心原理、兼容性检查方法,以及我踩过的坑,一次性讲清楚。
配置缓存到底在缓存什么?
先搞清楚一个问题:Gradle 构建分三个阶段——初始化、配置、执行。配置阶段会执行所有 build.gradle.kts 里的代码,创建 Task 对象、配置依赖、注册扩展等。如果项目有 100 个模块,每次构建都要重新跑一遍这些配置代码,耗时可想而知。
配置缓存的核心思路就是:把配置阶段的输出(Task 图、扩展属性、依赖关系等)序列化到磁盘。下次构建时,如果输入没有变化,直接反序列化恢复,跳过配置阶段。
关键点:缓存的是配置阶段的结果,不是配置代码本身。代码还是得解析,但执行结果可以复用。
来看一个简单的例子。假设你的 build.gradle.kts 里有这么一段:
tasks.register("hello") {
doLast {
println("Hello, ${project.name}!")
}
}
没有配置缓存时,每次运行 ./gradlew hello,Gradle 都要重新创建这个 Task 对象。有了配置缓存,第一次运行后,Task 图被缓存下来。第二次运行,只要 build.gradle.kts 没改,直接恢复缓存,瞬间完成配置。
兼容性检查:你的项目能用吗?
配置缓存不是银弹。它要求构建脚本和插件必须是可缓存的。什么叫可缓存?简单说就是:配置代码不能有副作用,不能依赖外部状态(比如当前时间、随机数、文件系统状态等)。
我建议你分三步走:
- 开启配置缓存:在 gradle.properties 里加一行
org.gradle.configuration-cache=true - 运行构建:执行你常用的任务,比如
./gradlew build - 检查报告:如果出现问题,Gradle 会生成一份兼容性报告,告诉你哪些地方不兼容
举个例子,我在一个项目里遇到过这样的问题:
// ❌ 不兼容:依赖了系统时间
val buildTime = System.currentTimeMillis()
tasks.register("printTime") {
doLast {
println("Build time: $buildTime")
}
}
配置缓存要求配置阶段的输入是可预测的。系统时间每次都不一样,Gradle 无法判断缓存是否有效。正确的做法是把时间计算放到执行阶段:
// ✅ 兼容:只在执行阶段获取时间
tasks.register("printTime") {
doLast {
println("Build time: ${System.currentTimeMillis()}")
}
}
小技巧:Gradle 提供了 --configuration-cache-problems=warn 参数,可以把不兼容问题降级为警告,而不是直接报错。适合在迁移初期使用。
常见问题与避坑指南
配置缓存虽然好,但坑也不少。我把自己遇到过的几个典型问题列出来,你遇到了可以直接对号入座。
1. 插件不兼容
这是最常见的问题。有些插件在配置阶段访问了文件系统、网络或系统属性,导致缓存失效。我曾经用过一个自定义插件,它在配置阶段读取了 ~/.gradle/gradle.properties:
// ❌ 插件内部:配置阶段读取外部文件
val props = File(System.getProperty("user.home"), ".gradle/gradle.properties")
.inputStream().use { it.readBytes() }
解决方案:要么升级插件版本(很多主流插件已经兼容了),要么给插件作者提 issue。如果实在没办法,可以临时关闭配置缓存:
// 在 build.gradle.kts 里按需关闭
if (gradle.startParameter.isConfigurationCache) {
gradle.startParameter.isConfigurationCache = false
}
警告:不要全局关闭配置缓存。建议只在有问题的模块或任务上关闭,用 configuration-cache.inputs 和 configuration-cache.outputs 来精细控制。
2. 自定义 Task 的输入声明不完整
配置缓存依赖 Task 的输入声明来判断缓存是否有效。如果你自定义的 Task 没有正确声明输入,Gradle 可能误判缓存命中,导致执行结果不对。
我记得有一次,我写了一个生成代码的 Task:
abstract class CodeGenerator : DefaultTask() {
@get:Input
abstract val templateFile: Property<String>
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@TaskAction
fun generate() {
// 读取模板文件,生成代码
}
}
问题出在 templateFile 只声明了文件名,但模板文件的内容变化没有被跟踪。正确的做法是:
@get:InputFile
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val templateFile: RegularFileProperty
这样 Gradle 就能通过文件的校验和来判断缓存是否过期。
3. 构建脚本中的惰性计算
配置缓存对惰性计算(Lazy Configuration)支持得很好。但如果你在配置阶段直接求值,就可能出问题。
举个例子:
// ❌ 配置阶段直接求值
val version = providers.gradleProperty("myVersion").get()
// ✅ 惰性求值,只在执行阶段获取
val version = providers.gradleProperty("myVersion")
前者在配置阶段就读取了属性值,如果属性值来自外部文件,缓存就会失效。后者把求值推迟到执行阶段,缓存就能正常工作。
配置缓存的核心逻辑
为了让你更直观地理解配置缓存的工作流程,我画了一张图:
从图上可以看得很清楚:第一次构建走完整流程,配置阶段执行完毕后序列化到缓存。第二次构建先检查缓存是否有效,有效就直接跳过配置阶段,无效则重新配置。
如何判断缓存是否命中?
Gradle 通过对比缓存键来判断。缓存键由以下因素组成:
| 因素 | 说明 | 示例 |
|---|---|---|
| 构建脚本内容 | 所有 build.gradle.kts 的校验和 | 修改了 dependencies 块 → 缓存失效 |
| Gradle 版本 | Gradle 发行版的哈希 | 从 7.0 升级到 7.1 → 缓存失效 |
| 系统属性 | 通过 -D 传入的属性 |
-DmyProp=value 变了 → 缓存失效 |
| 环境变量 | Gradle 认为相关的环境变量 | JAVA_HOME 变了 → 缓存失效 |
| Task 输入 | Task 声明的 @Input 注解属性 | 输入文件内容变了 → 缓存失效 |
调试技巧:运行 ./gradlew --configuration-cache-info 可以查看缓存命中/未命中的详细信息。如果缓存未命中,它会告诉你具体是哪个输入发生了变化。
我的实践经验总结
配置缓存不是开箱即用的。我建议你按这个顺序来:
- 先在小项目上试:选一个模块少、插件少的项目,开启配置缓存,看看能不能跑通
- 逐步扩大范围:如果小项目没问题,再推广到中型项目。我一般会先跑
./gradlew build --configuration-cache,看看有没有兼容性问题 - 处理不兼容问题:遇到不兼容的插件或脚本,优先升级版本。如果升级不了,再用
--configuration-cache-problems=warn临时绕过 - 监控缓存命中率:在 CI 里加上
--configuration-cache-info,定期检查缓存命中率。如果命中率低于 80%,说明有地方没配置好
我曾经在一个 50 多个模块的项目里推广配置缓存。刚开始命中率只有 60%,后来发现是一个自定义插件在配置阶段读取了 git describe 的输出。改成惰性求值后,命中率直接飙到 95% 以上,构建时间从 3 分钟降到了 40 秒。
嗯,配置缓存就是这样——前期需要花点心思适配,但一旦跑起来,收益非常可观。