让 Annotation Processor 支持增量编译

Java 的增量编译

Gradle 在 5.0 增加了对 Java 增量编译的支持,通过增量编译,我们能够获得一些优点:

  • 更少的编译耗时
  • 更少的字节码修改

增量编译概览:

  • Gradle 会重新编译受更改影响的所有类。
  • 如果某个类发生了更改或者它依赖的类发生了更改,那么这个类也会被重新编译。无论类是否在同一个项目、另一个项目,还是外部库中定义了的其他类。
  • 类的依赖关系由其字节码中的类型引用确定。
  • 由于常量可以内联,因此对常量的任何更改都将导致 Gradle 重新编译所有源文件。因此,我们应尽量减少在源代码中使用常量,并在可能的情况下间接通过静态方法返回常量。
  • 由于 Retention(SOURCE) 的注解不会编译进字节码中,因此对这种注解的更改将导致完全重新编译。
  • 可以通过应用软件设计原则(如松耦合)来提高增量编译性能。例如,当 类A 依赖 接口B 时,则仅当 接口B 更改时,才会重新编译依赖类 类A,而仅当 接口B 的某个实现类更改时,不会重新编译 类A
  • 对类分析的结果会缓存在项目目录中,因此在 CI 服务器上 checkout 后的首次构建可能会变慢,所以在 CI 服务器上一般关闭增量编译。

已知问题:

  • 如果编译过程中某个 task 造成了编译失败,下一次将会走完全编译
  • 如果使用的是读取外部资源的注释处理器(例如配置文件),则需要将这些资源声明为编译任务的输入
  • 如果资源文件发生修改,Gradle 会触发完全重编译

注解处理器增量编译

在 Gradle 编译时,会输出以下日志提醒你那些注解处理器没有支持增量编译:

w: [kapt] Incremental annotation processing requested, but support is disabled because the following processors are not incremental: com.x.XXProcessor (NON_INCREMENTAL).

让 APT 支持增量编译

首先 Gradle 支持两种注解处理器的增量编译:isolatingaggregating,你需要搞清你的注解处理器属于哪种。然后在 META-INF/gradle/incremental.annotation.processors 文件中声明支持增量编译的注解处理器。

比如我实现的一个支持增量编译的注解处理器的目录结构:

x_processor/src/main/
├── java
│   └── site
│       └── jiyang
│           └── features_impl_processor
└── resources
    └── META-INF
        ├── gradle
        │   └── incremental.annotation.processors
        └── services
            └── javax.annotation.processing.Processor

incremental.annotation.processorsjavax.annotation.processing.Processor 类似,一行一个注解处理器的声明:

<注解处理器全限定名>,isolating

同时要在后面声明注解处理器的类型,使用 , 号做分隔。

如果你的注解处理器要在运行时才能决定是否支持增量编译,那么可以声明为 *dynamic*,然后在注解处理器的 getSupportedOptions 方法中返回包含 org.gradle.annotation.processing.aggregatingSet<String>

incremental.annotation.processors:

<注解处理器全限定名>,dynamic
override fun getSupportedAnnotationTypes(): MutableSet<String> {
    return mutableSetOf(
        "<你的目标注解类名>", 
        "org.gradle.annotation.processing.aggregating"
    )
}

两种增量编译注解处理器的共同限制:

  1. 只能通过 javax.annotation.processing.Filer 接口去生成文件。任何其他方式生成的文件因为不能正确的被清理,会造成编译失败
  2. 要支持增量编译的注解处理器不能依赖编译器特有的类。因为 Gradle 包装了 processing API,任何依赖了特定编译器的类的编译都会失败
  3. 如果使用了 Filer#createResource, Gradle 将重编译所有源文件。

isolating

最快的注解处理器类别,这类注解处理器独立地搜索每个带注解的元素,并为其生成文件或验证消息。

限制

  • 这类 APT 从 AST(Abstract Synax Tree) 获得信息,为带注解的类做出所有决策(生成代码,编译检查等)。这意味着我们甚至可以递归地分析类的超类,方法返回类型,注解等。但是这类 APT 不能基于 RoundEnvironment 中不存在的元素进行决策。如果你的 APT 需要基于其他不相关元素的组合做出决策,你应该将它声明为 *aggregating*。

重新编译源文件时,Gradle 将重新编译由源文件生成的所有文件。 删除源文件后,从其生成的文件也会被删除。

aggregating

aggregating 类型的 APT, 可以将多个源文件聚合到一个或多个输出文件中。

限制

  • 这类 APT 只能读取 CLASS 或 RUNTIME 类型的会保留在字节码中的注解
  • 只能读到通过 -parameters 传递给编译器的参数的参数名

Gradle 将始终重新处理(但不会重新编译)APT 已处理的所有带注解的源文件。Gradle 将始终重新编译 APT 生成的任何文件。

KAPT 支持

Kotlin Annotation Processor 也支持了增量编译,在项目的 gradle.propertice 中声明如下配置就能开启:

kotlin.incremental=true

Auto-Service 配置

一些 APT 会使用 auto-service 去生成 *META-INF*,所以 auto-service 在 1.0.0-rc6 也支持了增量编译

参考