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%。

注意:构建缓存只缓存 Task 的输出,不缓存中间产物。如果你的 Task 有副作用(比如发邮件、写数据库),千万别用缓存,否则后果很严重。

7.3 知识体系总览

下面这张图帮你理清增量构建和构建缓存的关系:

Gradle 增量构建与缓存体系 Task 执行 输入变了? 跳过 Task (UP-TO-DATE) 执行 Task 缓存命中? 命中 从缓存恢复 (FROM-CACHE) 未命中 重新执行并缓存 增量构建:同一机器,多次构建 | 构建缓存:跨机器,共享成果

7.4 实战建议

说了这么多,最后给你几条我自己的经验:

  1. 先确认增量构建是否生效:运行 gradle build --info,看日志里有没有 UP-TO-DATE 字样。如果没有,说明你的 Task 没配好。
  2. 远程缓存一定要配认证:别把缓存暴露在公网上,否则别人能直接拿到你的构建产物。
  3. 定期清理本地缓存:Gradle 的缓存目录会越来越大,我一般设置 removeUnusedEntriesAfterDays = 30
  4. --build-cache 参数启用远程缓存:CI 脚本里加上这个参数,开发者本地默认只读本地缓存。
一句话总结:增量构建让你「改多少,编多少」;构建缓存让你「别人编过,你直接拿」。两者配合,构建速度能提升 80% 以上。

好了,这一章的内容就到这里。增量构建和缓存是 Gradle 性能优化的核心,值得你花时间好好配置。下一章我们聊聊更高级的话题——自定义 Task 和插件开发。


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