
从Gradle 4.7和Kotlin 1.3.30版本开始,由于注释的增量处理的正确操作,可以加速项目的增量组装。 在本文中,我们了解了Gradle中的增量编译理论在理论上是如何工作的,需要做什么以释放其全部潜能(同时又不丢失代码生成),以及在实践中可以通过激活注释的增量处理来实现增量汇编速度的哪种提高。
增量编译如何工作
Gradle中的增量构建在两个级别上实现。 第一级是使用避免编译操作取消重新编译模块的开始。 第二种是直接增量编译,仅在那些已更改或直接依赖于已更改文件的文件上,在一个模块的框架内启动编译器。
让我们考虑以下三个模块项目的示例(摘自Gradle的文章 )避免编译: app , core和utils 。
app模块的主要类(取决于core ):
public class Main { public static void main(String... args) { WordCount wc = new WordCount(); wc.collect(new File(args[0]); System.out.println("Word count: " + wc.wordCount()); } }
在核心模块中(取决于utils ):
public class WordCount {
在utils模块中:
public class IOUtils { void eachLine(File file, Callable<String> action) { try { try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
模块的第一次编译顺序如下(根据依赖关系的顺序):
1) 实用程序
2) 核心
3) 应用
现在考虑更改IOUtils类的内部实现时会发生什么:
public class IOUtils {
此更改不会影响ABI模块。 ABI(应用程序二进制接口)是组装后的模块的公共接口的二进制表示。 如果更改仅与模块的内部实现有关,并且不以任何方式影响其公共接口,则Gradle将使用避免编译并仅开始重新编译utils模块。 如果utils模块的ABI受到影响(例如,出现了一个附加的公共方法或现有签名的更改),则将另外开始核心模块的编译,但是如果依赖于内核的 应用程序模块通过实现进行连接,则不会以可传递方式重新编译依赖于核心的 应用程序模块。

在项目模块级别上避免编译的图示
第二个增量级别是直接在各个模块内部的已更改文件在编译器启动级别的增量。
例如,向核心模块添加一个新类:
public class NGrams {
在utils中 :
public class StringUtils { static String sanitize(String dirtyString) { ... } }
在这种情况下,在两个模块中,仅需重新编译两个新文件(不影响现有的且未更改的WordCount和IOUtils),因为新类和旧类之间没有依赖关系。
因此,增量编译器仅分析类之间的依赖关系并重新编译:
- 包含更改的类
直接依赖于变化的类的类
增量注释处理

使用APT和KAPT生成代码可以减少编写和调试样板代码的时间,但是注释处理可以显着增加构建时间。 更糟糕的是,长期以来,注释处理从根本上打破了Gradle中进行增量编译的可能性。
项目中的每个注释处理器都将其处理的注释列表告知编译器信息。 但是从组装的角度来看,注释处理是一个黑匣子:Gradle不知道处理器将执行什么操作,特别是它将生成哪些文件以及在何处生成。 在Gradle 4.7之前,使用注释处理器的那些源集会自动禁用增量编译。
随着Gradle 4.7的发布 ,增量编译现在支持注释处理,但仅适用于APT。 在KAPT中,Kotlin 1.3.30 引入了对增量注释的支持。 它还需要提供注释处理器的库的支持 。 注释处理器开发人员有机会明确设置处理器类别,从而将逐步编译工作所需的信息告知Gradle。
注释处理器类别
Gradle支持两类处理器:
隔离 -这样的处理器必须仅基于与特定批注的元素相关联的AST信息来做出代码生成的所有决策。 这是注释处理器最快的类别,因为如果源文件中没有更改,则Gradle可能不会重新启动处理器并使用它先前生成的文件。
聚集 -用于基于多个输入(例如,一次分析多个文件中的注释或基于对AST的研究,可以从带注释的元素传递而来的AST)进行决策的处理器。 每次Gradle都会为使用聚合处理器注释的文件启动处理器,但是如果它们没有更改,则不会重新编译它生成的文件。
对于许多基于代码生成的流行库,增量编译支持已在最新版本中实现。 请在此处查看支持它的库列表。
我们实施增量注释处理的经验
现在,对于从头开始并使用最新版本的库和gradle插件的项目,默认情况下最有可能启用增量构建。 但是,通过提高大型和长期项目的注释处理的增量,可以最大程度地提高装配效率。 在这种情况下,可能需要进行大量版本更新。 在实践中值得吗? 让我们来看看!
因此,为了使注释的增量处理正常工作,我们需要:
- 摇篮4.7+
- Kotlin 1.3.30+
- 我们项目中的所有注释处理器都必须有其支持。 这非常重要,因为如果在单个模块中至少有一个处理器不支持增量功能,则Gradle将为整个模块禁用它。 每次将再次编译模块中的所有文件! 在不升级版本的情况下获得对增量编译的支持的替代方法之一是在单独的模块中使用注释处理器删除所有代码。 在没有注释处理器的模块中,增量编译可以正常工作
为了检测不满足最后条件的处理器,可以使用标志-Pkapt.verbose = true运行程序集。 如果强制Gradle为单个模块禁用增量注释处理,那么在构建日志中,我们将看到一条消息,说明正在发生的处理器和模块(请参阅任务名称):
> Task :common:kaptDebugKotlin w: [kapt] Incremental annotation processing requested, but support is disabled because the following processors are not incremental: toothpick.compiler.factory.FactoryProcessor (NON_INCREMENTAL), toothpick.compiler.memberinjector.MemberInjectorProcessor (NON_INCREMENTAL).
在我们的带有非增量注释处理器的图书馆项目中,有3个:
幸运的是,这些库受到了积极支持,并且它们的最新版本已经具有增量支持。 此外,这些库的最新版本中的所有注释处理器都具有最佳类别-隔离。 在提高版本的过程中,由于Toothpick库API的更改,我不得不处理重构,这几乎影响了我们模块中的每个模块。 但是在这种情况下,我们很幸运,事实证明它是使用所使用的公共库方法的自动替换名称完全自动重构的。
请注意,如果您使用Room库,则需要将room.incremental:true标志显式传递给注释处理器。 一个例子 。 将来,Room开发人员计划默认启用此标志。
对于Kotlin 1.3.30-1.3.50版本,必须通过项目的gradle.properties文件中的kapt.incremental.apt = true 明确支持对注释的增量处理。 从版本1.3.50开始,默认情况下此选项设置为true。
增量装配分析
提出所有必要依赖项的版本之后,就该测试增量构建的速度了。 为此,我们使用了以下工具和技术:
- Gradle构建扫描
- gradle-profiler
- 要运行启用和禁用增量注释处理的脚本,请使用gradle属性kapt.incremental.apt = [true | false]
- 为了获得一致且有益的结果,在单独的CI环境中提出了程序集。 使用gradle-profiler复制了构建增量
gradle-profiler允许声明性地为增量构建基准测试准备脚本 。 根据以下条件编制了4个方案:
- 修改文件影响/不影响其ABI
- 支持打开/关闭增量注释处理
每个方案的运行都是以下顺序:
- 重新启动gradle守护程序
- 启动热身构建
- 运行10个增量程序集,然后在每个程序集上通过添加新方法来更改文件(非ABI更改为私有,ABI更改为公共)
所有构建均使用Gradle 5.4.1完成。 更改涉及的文件是项目的核心模块之一(通用),其中直接依赖40个模块(包括核心和功能)。 该文件使用注释来隔离处理器。
还值得注意的是,基准测试是在两项gradle任务上执行的: ompileDebugSources和assembleDebug 。 第一个仅使用源代码启动文件的编译,而无需使用资源进行任何工作并将应用程序捆绑到.apk文件中。 基于增量编译仅影响.kt和.java文件这一事实 ,选择了compileDedugSource任务以实现更隔离和更快的基准测试。 在实际开发条件下,当您重新启动应用程序时,Android Studio使用assembleDebug任务,该任务包括应用程序的完整调试版本。
基准结果
在gradle-profiler生成的所有图形中,垂直轴显示增量装配时间(以毫秒为单位),水平轴显示装配开始编号。
:在更新注释处理器之前先编译compileDebugSource

在将注释处理器更新为支持增量版本之前,每种方案的平均运行时间为38秒。 在这种情况下,Gradle将禁用对增量编译的支持,因此脚本之间没有显着差异。
:更新注释处理器之后的compileDebugSource

由于增加而导致的组装时间减少的中位数,对于ABI变更为31%,对于非ABI变更为32.5%。 绝对值,大约10秒。
:更新注释处理器后的assembleDebug

要在我们的项目上构建应用程序的完整调试版本,由于增加而导致的构建时间中位数减少对于ABI更改为21.5%,对于非ABI更改为23%。 绝对而言,大约是10秒,因为源代码的编译增量不会影响资源的汇编速度。
Gradle构建扫描中的构建扫描解剖
为了更深入地了解增量编译期间如何实现增量,我们比较了增量和非增量程序集的扫描。
在禁用KAPT增量的情况下,构建时间的主要部分是应用模块的编译,该模块无法与其他任务并行化。 非增量KAPT的时间表如下:

任务执行:在这种情况下,我们的应用模块的kaptDebugKotlin大约需要8秒钟。
启用KAPT增量的情况的时间表:

现在,该应用程序模块已在不到一秒钟的时间内重新编译。 值得注意的是,上图中两个扫描的比例尺在视觉上不成比例。 在第一个映像中看起来较短的任务不一定在第二个映像中看起来较长的任务中较长。 但是非常值得注意的是,当您打开增量KAPT时,应用程序模块的重新编译比例降低了多少。 在我们的案例中,我们在此模块上赢得了大约8秒的时间,在并行编译的较小模块上又赢得了大约2秒的时间。
同时,对于禁用的处理注释增量,所有* kapt任务的总执行时间为1分36秒,而打开时为55秒。 即,在不考虑模块的并行组装的情况下,收益更大。
还值得注意的是,以上基准测试结果是在CI环境下准备的,该环境能够运行24个并行线程进行组装。 在8线程环境中,在我们的项目中启用增量注释处理的收益约为20-30秒。
增量vs(?)并行
显着加快组装速度(增量和清洁)的另一种方法是通过将项目拆分为大量松散耦合的模块来并行执行gradle任务。 与使用增量式KAPT相比,模块化以一种或另一种方式代表了极大的加速装配的潜力。 但是项目越单一,并且使用的代码生成越多,注释的增量处理就越大。 获得程序集的完全增量性效果要比将应用程序分为多个模块要容易得多。 但是,这两种方法并不矛盾,并且可以完美地互补。
总结
- 在我们的项目中包含注释的增量处理,使我们能够将本地重建速度提高20%
- 为了启用增量注释处理,研究当前程序集的完整日志并查找带有文本“请求增量注释处理,但由于以下处理器不是增量处理器而被禁用的支持”的警告消息将非常有用。 有必要将库的版本升级到支持注释的增量处理的版本,并具有Gradle 4.7 +,Kotlin 1.3.30+版本
材料以及该主题的内容