22、Gradle Enterprise与构建缓存:构建缓存原理、远程缓存配置、缓存命中率优化、构建扫描分析、团队协作

说到Gradle构建,很多团队一开始都挺乐观的——写个脚本,配个依赖,跑起来就完事了。但随着项目膨胀,你会发现每次构建都要等上几分钟甚至十几分钟。这时候,构建缓存就成了救命稻草。

我个人习惯把构建缓存比作「记忆」。你做过的事情,如果结果没变,就别再重复做一遍。Gradle的构建缓存,说白了就是把这个「记忆」存下来,下次直接复用。

构建缓存原理:它到底在缓存什么?

很多人以为构建缓存就是缓存编译后的class文件。其实不止。Gradle的构建缓存缓存的是任务的输出。每个Task执行完后,它的输出会被打上「输入指纹」——也就是输入文件、参数、依赖版本等信息的哈希值。下次执行时,如果输入指纹完全一致,Gradle就直接从缓存里拿结果,跳过任务执行。

我在项目中遇到过一个问题:明明代码没改,但每次构建都重新编译。查了半天,发现是某个插件在每次构建时生成了一个时间戳文件,导致输入指纹变了。嗯,这种坑真的很隐蔽。

核心要点:构建缓存不是「增量编译」,而是「任务级缓存」。增量编译只针对编译任务,而构建缓存可以缓存任何Task的输出——包括代码生成、资源处理、AAR打包等。

为什么会这样?因为Gradle把每个Task都看作一个「黑盒」。只要输入不变,输出就应该不变。这个设计思路其实很优雅——你不需要关心Task内部做了什么,只需要保证输入是可复现的。

远程缓存配置:让团队共享缓存

本地缓存只能加速你自己的构建。但团队协作时,如果每个人都要自己「热缓存」,那效率还是上不去。远程缓存就是解决这个问题的。

远程缓存的配置其实不复杂。以Gradle Enterprise为例,你需要在settings.gradle.kts里加上插件和配置:

// settings.gradle.kts
plugins {
    id("com.gradle.enterprise") version("3.15.1")
}

gradleEnterprise {
    server = "https://你的企业服务器地址"
    buildCache {
        remote(HttpBuildCache) {
            url = uri("https://你的缓存服务器地址/cache/")
            credentials {
                username = "缓存用户名"
                password = "缓存密码"
            }
            push = true // 是否允许上传缓存
        }
    }
}

这里有个细节:push = true表示你的构建结果会上传到远程缓存。我建议只在CI/CD环境里开启push,开发机只拉取不上传。否则每个人都在上传,缓存服务器压力会很大。

我的经验:远程缓存服务器最好用SSD,网络带宽要够。我曾经见过一个团队,远程缓存配了但没人用,因为从远程拉缓存比本地重新编译还慢——那缓存还有什么意义?

缓存命中率优化:怎么让缓存「打中」?

缓存命中率低,说白了就是「输入指纹总在变」。我总结了几种常见原因:

  • 绝对路径依赖:有些Task依赖了文件的绝对路径,比如/home/user/project/...。换个机器路径就变了,缓存肯定不命中。
  • 时间戳/随机数:Task里生成时间戳或UUID,每次都不一样,缓存直接失效。
  • 依赖版本不固定:用了+号版本号,比如com.example:lib:1.+。每次解析的版本可能不同,缓存就乱了。
  • BuildConfig字段:如果BuildConfig里包含了构建时间、Git Commit等动态值,每次构建都会变。

怎么优化?我建议从这几个方向入手:

  1. 固定依赖版本:用versions.toml统一管理,别用动态版本。
  2. 避免Task生成动态内容:如果必须生成时间戳,把它放到一个独立的Task里,并且不要缓存这个Task。
  3. 使用规范化路径:Gradle的relativePath()可以帮你把绝对路径转成相对路径。
  4. 开启缓存调试:在gradle.properties里加上org.gradle.caching.debug=true,可以看到每个Task的缓存命中情况。
避坑指南:我曾经在项目里用了Kotlin的@JvmStatic注解,结果每次构建都会重新生成一个匿名类,导致缓存永远不命中。排查了整整两天才发现。所以,如果你用了代码生成或注解处理器,一定要检查它们的输出是否稳定。

构建扫描分析:用数据说话

构建扫描是Gradle Enterprise提供的可视化工具。它能告诉你每个Task花了多少时间、缓存是否命中、依赖解析耗时等。我个人觉得,构建扫描是优化构建速度的「第一手资料」。

你可以在构建命令后加上--scan参数:

./gradlew assembleDebug --scan

执行完后,终端会输出一个链接。打开它,你会看到类似这样的信息:

指标 说明 优化方向
Task执行时间 每个Task的耗时 找出耗时最长的Task,看能否缓存或优化
缓存命中率 从缓存中恢复的任务占比 如果低于80%,检查输入指纹变化原因
依赖解析时间 解析依赖库的时间 使用dependency locking或固定版本
配置阶段时间 Gradle配置脚本的执行时间 减少allprojectssubprojects中的逻辑

你想想看,如果没有构建扫描,你只能靠猜来优化构建。有了数据,你就能精准定位问题。我每次接手一个新项目,第一件事就是跑一次--scan,看看构建的「体检报告」。

团队协作:让缓存成为团队习惯

构建缓存不是配好了就完事的。它需要团队共同维护。我见过不少团队,配了远程缓存,但没人用——因为大家不知道缓存的好处,或者不知道怎么用。

我建议从这几个方面入手:

  • 统一Gradle版本:团队所有成员和CI使用同一个Gradle版本,否则缓存可能不兼容。
  • 规范Task输入:写自定义插件时,明确声明@Input@Output,不要漏掉任何输入。
  • 定期审查构建扫描:每周或每两周看一次构建扫描,发现缓存命中率下降就及时排查。
  • CI/CD优先使用缓存:CI环境通常从零开始构建,远程缓存能大幅缩短构建时间。
一个小技巧:在CI脚本里,可以先用--build-cache拉取远程缓存,如果缓存命中,构建时间能从10分钟降到1分钟。如果没命中,再走完整构建。这样既保证了速度,又不会因为缓存问题导致构建失败。

最后,我想说一句:构建缓存不是银弹。它解决的是「重复劳动」的问题,但如果你本身构建逻辑就有问题——比如Task设计不合理、依赖关系混乱——那缓存也救不了你。先把构建脚本写干净,再考虑缓存优化,这才是正路。

构建缓存知识体系 构建缓存 缓存原理 任务输出缓存 输入指纹哈希 跳过任务执行 远程缓存配置 Gradle Enterprise HttpBuildCache push/pull策略 命中率优化 固定依赖版本 避免动态内容 规范化路径 构建扫描分析 --scan参数 Task耗时分析 缓存命中率报表 团队协作:统一版本 + 规范Task + 定期审查 缓存不是银弹,先优化构建逻辑,再考虑缓存
公众号:蓝海资料掘金营,微信deep3321