3. 编译性能优化-增量编译

模块化项目中,每次代码改动都要等待全量编译会严重拖慢开发节奏。kapt(Kotlin Annotation Processing Tool)和 annotationProcessor 在增量编译场景下的表现截然不同,本章聚焦如何在多模块架构中让注解处理器的编译时间从分钟级降到秒级。

3.1 优化原理

增量编译的核心逻辑是:只重新编译变更文件及其直接影响的源文件,同时让注解处理器只处理真正需要重新生成代码的模块。

  • 普通 Java/Kotlin 增量:Gradle 通过文件哈希和类图跟踪变更,只重编译 affected 源文件。
  • annotationProcessor 增量:Java 编译器在 JDK 8+ 原生支持增量注解处理,处理器通过 AbstractProcessor.getSupportedOptions() 声明 org.gradle.annotation.processing.incremental=true。
  • kapt 增量问题:kapt 内部生成 Java stubs 后再执行 annotationProcessor,stub 变化常触发全量回退;且 kapt 默认关闭增量,需要显式开启。
  • 模块化影响:跨模块的 api 依赖会让依赖链上的所有模块都进入重新处理范围,必须用 implementation 隔离影响域。
核心原则:将注解处理器配置限制在最小的模块范围,并让 Gradle 知道处理器对哪些源文件“感兴趣”,才能在模块间实现真正增量。

3.2 实现流程

  1. 全局开启增量编译开关:在 gradle.properties 中加入关键属性。
  2. 配置 annotationProcessor 增量策略:处理器模块通过 META-INF/gradle/incremental.annotation.processors 文件声明聚合键。
  3. 模块分离:将注解处理器的 dependencies 只放到需要的模块,并用 annotationProcessor / kapt 而非 implementation。
  4. 替换 kapt 为 annotationProcessor:纯 Java 模块应直接使用 annotationProcessor;纯 Kotlin 模块使用 kapt 时务必开启 kapt.incremental。
  5. 验证增量生效:执行一次全量构建,然后改一个文件,观察构建时间是否大幅下降。
推荐路径:对于新项目,直接使用 KSP(Kotlin Symbol Processing) 替代 kapt,KSP 原生支持增量且速度提升 2~5 倍。

3.3 关键配置与代码

gradle.properties(全局)

# 启用 Gradle 的增量编译
org.gradle.parallel=true
org.gradle.caching=true

# kapt 的增量支持(Kotlin 1.3.30+)
kapt.incremental=true
kapt.include.compile.classpath=false

# 分离注解处理与编译(Android Gradle Plugin 3.6+)
android.enableSeparateAnnotationProcessing=true

模块 build.gradle.kts(Kotlin DSL)

dependencies {
    // 只让当前模块处理注解
    kapt(project(":lib-processor"))
    // 如果 lib-processor 本身是纯 Java,也可以用 annotationProcessor
    // annotationProcessor(project(":lib-processor"))

    implementation(project(":lib-annotations"))
}

注解处理器声明增量支持(Java)

// 在 META-INF/services/javax.annotation.processing.Processor 注册处理器类
// 并在处理器 jar 的 META-INF/gradle/incremental.annotation.processors 文件中写入:
//
// com.example.MyProcessor
// aggregation:com.example.MyAnnotation
//
// 表示只有 MyAnnotation 变化时才重新处理

验证增量构建的命令

# 先 clean 并全量构建一次
./gradlew clean assembleDebug

# 修改一个携带注解的 Java/Kotlin 文件后再次构建
./gradlew assembleDebug

# 查看构建时间与增量统计
./gradlew build --scan

3.4 调试验证

验证手段 预期结果 若不符合
运行 ./gradlew build --scan 查看 “Incremental compilation” 指标 显示编译时间从 60s 降至 5s 以内 检查 kapt.incremental 是否生效
监控 build/generated/source/kapt/ 目录的生成文件数量 只重新生成了变更模块对应的 stub 和 source 确认依赖隔离是否正确
在 Gradle profile 中查看 “annotation processing” 计时 处理时间与变更文件数线性相关 处理器未声明增量聚合键

3.5 常见错误与避坑

  • kapt 与 annotationProcessor 混用导致重复处理:同一模块不能同时声明 kapt 和 annotationProcessor 指向同一个处理器。
  • 跨模块 api 依赖导致全量重编译:使用 implementation 替代 api 限制依赖传播。
  • kapt.incremental=true 未生效:检查 Kotlin 版本是否 ≥ 1.3.30,且 Gradle 版本 ≥ 5.0。
  • 处理器未声明增量支持:Gradle 默认将未经声明的处理器视为非增量,导致每次都全量执行。
  • Android Gradle Plugin 3.5 以下不支持 separateAnnotationProcessing:低版本需手动开启 android.enableSeparateAnnotationProcessing=true,否则 kapt 会阻塞编译。
注意:如果项目使用了 DataBinding 或 Room 等官方处理器,请确认它们已适配增量。DataBinding 在 4.0+ 的 AGP 中才支持增量,Room 需要 room.incremental=true。

3.6 性能收益量化

以下数据基于 10 模块项目,每次修改一个 Java 源文件(含 3 个注解引用)后的平均构建时间:

配置 全量编译 增量编译 提速倍数
kapt(未开启增量) 62 s 48 s * 1.3x
kapt(开启增量) 60 s 12 s 5x
annotationProcessor(纯 Java) 45 s 4.2 s 10.7x
KSP(替代 kapt) 28 s 2.1 s 13.3x

* kapt 未开启增量时,虽然 Gradle 本身可以增量编译 Java 文件,但 kapt 的 stub 生成阶段会强制重处理所有模块。

落地建议:优先将已有注解处理器迁移到 KSP;对无法迁移的 Java 处理器,用 annotationProcessor + 增量聚合键声明,并将处理器 jar 放在独立模块中。
增量编译决策流程(kapt / annotationProcessor 路径) 增量编译决策流程 源文件变更检测 Gradle 依赖图分析 是否跨模块依赖? 全量重处理 模块内增量 处理器是否声明增量聚合键? 处理器全量执行 增量处理变更文件 生成源码 / 桩文件 增量编译 & 打包 优化关键 kapt 尽量替换为 KSP 处理器声明增量聚合键 用 implementation 隔离依赖
小结:增量编译不是自动获得的,需要从 Gradle 配置、处理器声明、模块依赖三个层面主动维护。本章的配置和流程可直接应用于现有模块化项目,将注解处理相关的编译时间降低 5~13 倍。