配置缓存:配置缓存原理、兼容性检查、常见问题

配置缓存(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 没改,直接恢复缓存,瞬间完成配置。

兼容性检查:你的项目能用吗?

配置缓存不是银弹。它要求构建脚本和插件必须是可缓存的。什么叫可缓存?简单说就是:配置代码不能有副作用,不能依赖外部状态(比如当前时间、随机数、文件系统状态等)。

我建议你分三步走:

  1. 开启配置缓存:在 gradle.properties 里加一行 org.gradle.configuration-cache=true
  2. 运行构建:执行你常用的任务,比如 ./gradlew build
  3. 检查报告:如果出现问题,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.inputsconfiguration-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/configuration-cache) 第二次构建 检查缓存是否有效 命中 → 跳过配置阶段 未命中 → 重新配置 缓存命中 缓存未命中

从图上可以看得很清楚:第一次构建走完整流程,配置阶段执行完毕后序列化到缓存。第二次构建先检查缓存是否有效,有效就直接跳过配置阶段,无效则重新配置。

如何判断缓存是否命中?

Gradle 通过对比缓存键来判断。缓存键由以下因素组成:

因素 说明 示例
构建脚本内容 所有 build.gradle.kts 的校验和 修改了 dependencies 块 → 缓存失效
Gradle 版本 Gradle 发行版的哈希 从 7.0 升级到 7.1 → 缓存失效
系统属性 通过 -D 传入的属性 -DmyProp=value 变了 → 缓存失效
环境变量 Gradle 认为相关的环境变量 JAVA_HOME 变了 → 缓存失效
Task 输入 Task 声明的 @Input 注解属性 输入文件内容变了 → 缓存失效

调试技巧:运行 ./gradlew --configuration-cache-info 可以查看缓存命中/未命中的详细信息。如果缓存未命中,它会告诉你具体是哪个输入发生了变化。

我的实践经验总结

配置缓存不是开箱即用的。我建议你按这个顺序来:

  1. 先在小项目上试:选一个模块少、插件少的项目,开启配置缓存,看看能不能跑通
  2. 逐步扩大范围:如果小项目没问题,再推广到中型项目。我一般会先跑 ./gradlew build --configuration-cache,看看有没有兼容性问题
  3. 处理不兼容问题:遇到不兼容的插件或脚本,优先升级版本。如果升级不了,再用 --configuration-cache-problems=warn 临时绕过
  4. 监控缓存命中率:在 CI 里加上 --configuration-cache-info,定期检查缓存命中率。如果命中率低于 80%,说明有地方没配置好

我曾经在一个 50 多个模块的项目里推广配置缓存。刚开始命中率只有 60%,后来发现是一个自定义插件在配置阶段读取了 git describe 的输出。改成惰性求值后,命中率直接飙到 95% 以上,构建时间从 3 分钟降到了 40 秒。

嗯,配置缓存就是这样——前期需要花点心思适配,但一旦跑起来,收益非常可观。


公众号:蓝海资料掘金营,微信 deep3321