5月27日 23:19

Gradle 的增量构建是如何工作的?

核心机制

Gradle 增量构建通过跟踪任务的输入(inputs)和输出(outputs)的哈希值来决定是否跳过执行。当输入未变化且输出存在时,任务标记为 UP-TO-DATE 并跳过;只有输入变化或输出缺失时才重新执行。

关键点:

  • 首次构建记录所有输入输出的快照(哈希值)
  • 后续构建比较当前快照与历史快照
  • 增量任务可进一步获取文件级变更细节(新增/修改/删除),只处理变化部分

输入输出声明

没有正确声明输入输出的任务无法参与增量构建:

groovy
abstract class ProcessTask extends DefaultTask { @InputDirectory abstract DirectoryProperty getInputDir() @OutputDirectory abstract DirectoryProperty getOutputDir() @TaskAction void execute(InputChanges changes) { if (changes.incremental) { changes.getFileChanges(inputDir).each { change -> // change.changeType: ADDED / MODIFIED / REMOVED // 只处理变化的文件 } } else { // 全量处理 } } }

运行时 API 方式(无需自定义 Task 类):

groovy
tasks.register('processFiles') { inputs.dir('src/main/resources') outputs.dir('build/processed') doLast { /* ... */ } }

构建性能优化手段

1. 启用增量编译与构建缓存

properties
# gradle.properties org.gradle.caching=true org.gradle.parallel=true

2. 远程构建缓存(CI 环境)

groovy
buildCache { remote(HttpBuildCache) { url = 'https://cache.example.com/cache/' push = System.getenv('CI') != null } }

3. 配置缓存(Configuration Cache):缓存项目配置阶段结果,避免每次构建重新解析 build.gradle,Gradle 8.x 已趋于稳定。

4. 避免配置阶段耗时操作:将逻辑移入 doLast / doFirst,而非任务配置闭包中。

常见增量构建失效的原因

  • 输入输出未声明或声明不全 → 任务每次都执行
  • 输出被外部进程修改 → 缓存校验失败
  • 任务存在非确定性输出(如含时间戳) → 同样输入产生不同输出
  • outputs.upToDateWhen 中包含非确定性逻辑

调试命令:

bash
./gradlew build --info # 查看 UP-TO-DATE 原因 ./gradlew build --rerun-tasks # 强制全量执行 ./gradlew build --scan # 生成构建分析报告

追问

Q: 增量构建和构建缓存有什么区别? 增量构建在同一项目内比较输入输出快照跳过任务;构建缓存可以跨项目、跨机器复用任务输出,存储在本地或远程。两者互补:增量构建避免本地重复执行,构建缓存避免跨环境重复执行。

Q: 输入归一化(Input Normalization)是什么? Gradle 比较输入时会对 classpath 做归一化处理——忽略 JAR 中的时间戳和顺序差异,只比较实际内容。可通过 @Classpath@CompileClasspath 注解控制归一化策略,减少不必要的任务重新执行。

Q: 配置缓存对增量构建有什么影响? 配置缓存缓存的是配置阶段的结果(任务图、属性值),增量构建缓存的是执行阶段的输入输出快照。两者作用于不同阶段,同时启用效果叠加。但配置缓存要求构建脚本无副作用,部分插件可能不兼容。

标签:Gradle