7、增量构建与缓存:输入输出注解、增量构建原理、构建缓存配置
聊到 Gradle 构建,很多人第一反应就是「慢」。尤其是项目大了以后,每次改一行代码都要等两三分钟,确实让人抓狂。其实 Gradle 早就内置了一套增量构建和缓存机制,只是很多人没用好。今天我就带你彻底搞懂它。
7.1 增量构建:只做必要的事
增量构建说白了就是:输入没变,输出就不重新生成。Gradle 会跟踪每个 Task 的输入和输出,如果发现输入没变化,就直接跳过这个 Task。
我刚开始用 Gradle 时,总觉得它「智能得有点玄学」。后来踩过坑才明白,它的判断逻辑其实很朴素——就看输入输出有没有变。
7.1.1 输入输出注解
要让 Gradle 识别增量构建,你得在自定义 Task 里正确标注输入和输出。常用的注解有这些:
| 注解 | 作用 | 示例 |
|---|---|---|
@Input |
标记单个输入值 | 版本号、开关参数 |
@InputFile |
标记单个输入文件 | 配置文件 |
@InputDirectory |
标记输入目录 | 源码目录 |
@InputFiles |
标记多个输入文件 | 依赖的 JAR 列表 |
@OutputFile |
标记单个输出文件 | 生成的 APK |
@OutputDirectory |
标记输出目录 | 编译后的 class 目录 |
@Incremental |
支持增量处理 | 配合 @InputFiles 使用 |
看个实际例子:
abstract class MyTransformTask : DefaultTask() {
@get:InputFiles
@get:Incremental
abstract val inputFiles: ConfigurableFileCollection
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@TaskAction
fun process() {
// 这里可以拿到变更的文件列表
val changes = inputFiles.asFileTree.files
// 只处理变更的文件
changes.forEach { file ->
// 处理逻辑
}
}
}
嗯,这里要注意:@Incremental 注解不是随便加的。它要求你的 Task 能处理「部分文件变更」,而不是每次都全量处理。我见过不少项目加了注解但没改逻辑,结果增量构建根本没生效。
7.1.2 增量构建原理
Gradle 是怎么判断「输入变了」的?它其实维护了一个快照。每次构建结束后,Gradle 会记录所有输入输出的哈希值。下次构建时,对比哈希值,没变就跳过。
为什么会这样设计?因为哈希对比比重新执行 Task 快得多。你想想看,一个 Task 可能执行 10 秒,但哈希对比只要几毫秒。
我曾经遇到过一个坑:某个 Task 的输入是一个目录,但目录里有个临时文件每次都会变。结果 Gradle 认为输入变了,每次都重新执行。后来我把临时文件排除掉,增量构建就正常了。
7.2 构建缓存:跨机器共享成果
增量构建解决的是「同一台机器上,多次构建」的问题。但如果你有 CI/CD,或者团队多人协作,每次都在各自机器上重新构建,那还是慢。这时候就需要构建缓存。
构建缓存可以把 Task 的输出存起来,下次不管在哪台机器上,只要输入一样,就直接从缓存里拿结果。
7.2.1 本地缓存 vs 远程缓存
| 类型 | 存储位置 | 适用场景 |
|---|---|---|
| 本地缓存 | ~/.gradle/caches/ |
个人开发机,多次构建 |
| 远程缓存 | HTTP 服务器、S3、Nexus 等 | CI/CD、团队协作 |
本地缓存默认就开启了,你不需要额外配置。但远程缓存需要手动设置。
7.2.2 配置远程缓存
在 settings.gradle.kts 里配置:
buildCache {
local {
isEnabled = true
directory = File(rootDir, ".build-cache")
removeUnusedEntriesAfterDays = 30
}
remote(HttpBuildCache::class) {
url = uri("https://cache.example.com/cache/")
isPush = true
credentials {
username = "cache-user"
password = "cache-pass"
}
}
}
我个人习惯把远程缓存搭在内部服务器上,这样 CI 构建的结果可以被开发者直接复用。有一次我们团队改了一个公共库,CI 构建花了 20 分钟,但开发者拉下来只用了 2 秒——因为缓存命中了。
isPush = true,这样 CI 构建的结果能自动上传。开发者那边可以只设置 isPush = false,只读缓存,避免误操作。
7.2.3 缓存命中率优化
缓存不是万能的。如果 Task 的输入包含「不稳定」的东西,缓存命中率就会很低。常见的坑有:
- 时间戳:不要把当前时间作为输入
- 随机数:不要用随机值作为参数
- 绝对路径:不同机器路径不同,缓存会失效
- Git 分支名:分支变了,缓存就没了
我曾经遇到一个案例:某个 Task 把 System.currentTimeMillis() 作为输入参数,结果每次构建缓存都 miss。排查了半天才发现,改掉后缓存命中率直接飙到 90%。
7.3 知识体系总览
下面这张图帮你理清增量构建和构建缓存的关系:
7.4 实战建议
说了这么多,最后给你几条我自己的经验:
- 先确认增量构建是否生效:运行
gradle build --info,看日志里有没有UP-TO-DATE字样。如果没有,说明你的 Task 没配好。 - 远程缓存一定要配认证:别把缓存暴露在公网上,否则别人能直接拿到你的构建产物。
- 定期清理本地缓存:Gradle 的缓存目录会越来越大,我一般设置
removeUnusedEntriesAfterDays = 30。 - 用
--build-cache参数启用远程缓存:CI 脚本里加上这个参数,开发者本地默认只读本地缓存。
好了,这一章的内容就到这里。增量构建和缓存是 Gradle 性能优化的核心,值得你花时间好好配置。下一章我们聊聊更高级的话题——自定义 Task 和插件开发。