14、构建缓存:本地构建缓存、远程构建缓存、缓存键规则、避免缓存污染
构建缓存这东西,说白了就是「让 Gradle 记住上次干活的结果」。你想想看,每次构建都要重新编译、打包、测试,很多任务其实根本没变过。我刚开始用 Gradle 的时候,就发现每次改一行代码,整个项目都要重来一遍,那叫一个慢。
后来我仔细研究了缓存机制,才明白——Gradle 的缓存不是简单的文件缓存,它是一套基于「输入」和「输出」的智能复用系统。只要任务的输入没变,输出就可以直接拿来用。
核心思想:构建缓存 = 任务输出的快照。每个任务执行完后,Gradle 会把它的输出打包成一个「缓存条目」。下次再跑同样的任务,如果输入没变,就直接从缓存里拿结果,跳过执行。
14.1 本地构建缓存
本地缓存,就是存在你电脑上的缓存。默认情况下,Gradle 会在 ~/.gradle/caches/ 目录下建一个 build-cache-1 文件夹。所有缓存条目都放里面。
我个人习惯把本地缓存看作「个人加速器」。你一个人开发的时候,本地缓存就够了。改一行代码,重新构建,只有改动的模块需要重新编译,其他模块直接从缓存拿。
小技巧:如果你发现本地缓存越来越大,可以用 gradle cleanBuildCache 命令清空。我一般每个月清一次,释放几个 G 的磁盘空间。
本地缓存的配置很简单,在 gradle.properties 里加一行:
org.gradle.caching=true
嗯,就这么简单。但要注意,这个开关默认是关闭的。我记得有一次帮同事排查构建慢的问题,发现他根本没开缓存……开了之后,构建时间从 3 分钟降到了 40 秒。
14.2 远程构建缓存
远程缓存,说白了就是「团队共享的缓存」。你想想看,团队里 10 个人,每个人都在本地编译一遍同样的代码,这不是浪费吗?远程缓存就是解决这个问题的。
Gradle 支持多种远程缓存后端:
- HTTP 缓存服务器(比如用 Nginx 搭建的)
- 云存储(比如 AWS S3、阿里云 OSS)
- Gradle Enterprise(商业版,功能最全)
配置远程缓存,需要在 settings.gradle 里加一段:
buildCache {
remote(HttpBuildCache) {
url = 'https://cache.mycompany.com/cache/'
push = true
}
}
这里有个坑——push = true 表示你的构建结果会上传到远程缓存。我建议只在 CI 服务器上开启 push,开发机只拉取不推送。否则每个人改个代码都往远程推,缓存会变得很乱。
注意:远程缓存不是银弹。如果你的网络延迟很高,从远程拉取缓存可能比本地重新构建还慢。我曾经遇到过这种情况,后来给远程缓存加了个超时设置:
remote(HttpBuildCache) {
url = 'https://cache.mycompany.com/cache/'
push = false
// 设置超时,避免网络问题拖慢构建
timeout = Duration.ofSeconds(30)
}
14.3 缓存键规则
缓存键,就是 Gradle 用来判断「这个任务的输入有没有变」的依据。说白了,每个任务都有一个唯一的缓存键,由以下因素决定:
- 任务类型(比如 JavaCompile、Test)
- 输入文件的内容哈希(不是文件名,是文件内容的哈希值)
- 任务参数(比如编译选项、JVM 参数)
- 依赖任务的输出(如果依赖的任务变了,当前任务也得重新跑)
为什么会这样设计?因为 Gradle 要确保缓存的结果和重新执行的结果完全一致。我见过有人把缓存键理解成「文件名+时间戳」,这是不对的。Gradle 用的是内容哈希,哪怕你改了文件名,内容没变,缓存依然有效。
来看一个自定义任务的缓存键配置:
abstract class MyTask extends DefaultTask {
@Input
abstract Property<String> getMyInput()
@InputFile
abstract RegularFileProperty getMyInputFile()
@OutputFile
abstract RegularFileProperty getMyOutputFile()
@TaskAction
void doWork() {
// 任务逻辑
}
}
这里 @Input 和 @InputFile 就是缓存键的组成部分。@OutputFile 是缓存的结果。Gradle 会自动计算这些注解标记的属性的哈希值,生成缓存键。
避坑指南:千万不要把 @OutputDirectory 标记的属性当作输入来用。我曾经犯过这个错——把输出目录当输入,结果每次构建缓存都失效,因为输出目录的内容一直在变。
14.4 避免缓存污染
缓存污染,就是缓存里存了「不该存的东西」。比如你把本地环境变量、时间戳、随机数这些不可控因素当成了任务的输入,那缓存键每次都不一样,缓存就形同虚设。
我遇到过最典型的例子:有个同事在自定义任务里用了 System.currentTimeMillis() 作为输入,结果每次构建缓存都 miss,构建时间从 1 分钟变成了 10 分钟。排查了半天才发现问题。
常见的缓存污染源:
| 污染源 | 原因 | 解决方案 |
|---|---|---|
| 绝对路径 | 不同机器路径不同,缓存键不一致 | 使用相对路径,或者用 @PathSensitive 注解 |
| 时间戳 | 每次构建时间不同,缓存键变化 | 移除时间戳依赖,或者用 @Ignore 注解 |
| 随机数 | 每次生成不同,缓存永远 miss | 用固定种子,或者不要作为输入 |
| 环境变量 | 不同机器环境不同 | 只把必要的环境变量作为输入 |
| Git 分支名 | 分支切换导致缓存失效 | 不要将分支名作为输入 |
还有一个容易被忽略的点——非确定性的任务。比如你的任务会从网络下载文件,或者依赖外部服务的响应。这些结果每次都可能不同,缓存了反而会出问题。对于这类任务,我建议直接禁用缓存:
tasks.named("myUnstableTask") {
outputs.cacheIf { false }
}
嗯,这样 Gradle 就不会缓存这个任务的输出了。
14.5 知识体系总览
下面这张图,是我梳理的构建缓存核心逻辑。你看一眼就能明白整个体系是怎么运转的:
你看,整个流程其实很清晰:任务执行前先算缓存键,然后依次查本地缓存、远程缓存,都没命中就老老实实重新执行。执行完后,再把结果存到本地和远程缓存里,方便下次使用。
总结一下:
- 本地缓存是个人加速器,开箱即用
- 远程缓存是团队加速器,需要搭建服务端
- 缓存键要稳定,避免把不可控因素加进去
- 缓存污染是最大的敌人,时刻警惕
最后说一句,构建缓存不是万能的。如果你的项目里有很多非确定性的任务,或者频繁改动的模块,缓存的效果会大打折扣。但话说回来,对于大部分项目,合理配置缓存能让构建速度提升 50% 以上。这笔账,怎么算都划算。