避免编译
引言
我们最近注意到社区里有一些关于通过忽略不影响依赖项ABI的更改来加速JVM上Gradle编译的讨论。这是一个很棒的想法!事实上,Gradle从3.4版本开始就默认对Java使用ABI,无需任何额外配置。我们将此功能称为避免编译。本文解释了基于ABI的编译对普通工作流意味着什么。剧透:利用避免编译是任何构建的最佳性能增强之一。
什么是应用程序二进制接口? #
应用程序二进制接口(ABI)是编译软件时生成的接口,它定义了内部和外部交互。ABI表示在编译时对消费者可见的内容。编译项目时,其任何依赖项的ABI中是否存在更改决定了编译是否最新或是否需要重新编译。这些ABI包含对消费者项目可见的所有关于依赖项的公共信息,例如:
- 任何带有参数类型和返回语句的公共方法
- 任何公共属性和字段
- 用于编译ABI的任何依赖项。
当一个人通过源代码访问库时,他们使用的是库的API。当机器访问已编译的二进制文件时,它使用的是ABI。
为什么ABI与构建性能相关? #
现代构建系统在编译代码时会考虑ABI兼容性,以便在编译代码库的增量更改时尽可能地避免编译。
对内部实现细节的更改是ABI兼容的:它们不会改变公共接口。实际上,项目的内部实现细节比公共组件更改得更频繁。当公共信息没有改变时,任何下游项目都不需要重新编译。跳过这些额外的工作可以对大型项目的构建性能产生巨大影响,无论是在本地还是在CI上。
对公共接口的更改是ABI不兼容的,因为它们改变了公共接口。ABI不兼容的更改需要重新编译所有下游依赖项。不得不重新编译ABI不兼容更改的所有下游依赖项会大大增加构建时间。
什么是避免编译? #
当发生ABI兼容的更改时,Gradle会优化构建。我们称这种优化为避免编译。
要了解这是如何工作的,请想象两个项目。:app
依赖于 :lib
。以下是我们通常可以对 :lib
进行的一些ABI兼容的更改,这些更改不会导致 :app
(以及任何依赖于 :app
的东西)需要重新编译:
- 对方法体进行任何更改
- 添加、删除或更改私有方法、字段或内部类
- 重命名参数
- 更改注释
- 更改类路径中jar或目录的名称
- 添加、删除或更改资源
当 :lib
中发生ABI兼容的更改,并且运行依赖于其类的‘:app’中的任务时,Gradle不会重新编译项目 :app
或任何依赖于 :app
的项目。在大型多项目构建中,这可以节省大量时间。
避免编译与增量编译有何不同? #
并非所有更改都符合上述要求。在某些情况下,您需要更改 :lib
的公共 ABI,这将导致需要编译 :app
。幸运的是,此时会使用另一个开箱即用的功能,称为增量编译。它将智能地重新编译 :lib
中已更改的类,这样下游的 :app
仍然比完全编译更快。
增量编译与避免编译不同,但非常互补。
“避免编译”是指完全避免为给定项目调用编译器。
另一方面,“增量编译”确实意味着调用编译器,但在这样做时试图减少需要重新编译的代码量。这通过在正常编译过程中跟踪类之间的引用,并且只重新编译受给定更改影响的事物来实现。
使用增量编译,我们会查看所有发生更改的类,而避免编译则查看整个项目。避免编译适用于依赖项目之间,而不仅仅像增量编译那样在项目内部。也就是说,增量编译优化了项目内各个类的编译。增量编译发生在一个项目内部,但避免编译关注多个项目之间的关系。增量编译通过减少需要重新编译的代码量,仍然可以节省跨项目的时间。
我正在使用增量编译还是避免编译? #
总而言之,如果您进行了ABI兼容的更改,那么您正在使用增量编译和避免编译:对于您进行更改的源文件的编译任务使用增量编译,对于下游项目使用避免编译。
或者,如果您的更改不是ABI兼容的,则只能从增量编译中受益。
ABI JAR呢? #
一些构建系统会生成ABI JAR以实现避免编译。有时也称为头文件JAR,它们包含整体接口而没有内部细节。ABI JAR仅包含公共方法、字段、常量和嵌套类型,所有方法体都被移除,可用于评估任何更改是否表明需要重新编译。使用Gradle,我们不需要ABI JAR,因为当有编译器任务时,我们会规范其输入并生成ABI的唯一哈希值。然后Gradle使用此哈希值来检查是否有任何更改。
不同语言的注意事项 #
Groovy有一个可选的实验性功能。
Kotlin有一个实验性功能,由JetBrains作为Kotlin Gradle插件的一部分开发。
我如何使用这个功能? #
Gradle自动使用避免编译。多年来,增量编译和避免编译在用Gradle构建的Java项目中默认启用。因此,下次您在编辑代码后担心重新编译时,请放心,Gradle会自动为您提供性能提升。
不确定您是否正在使用增量编译或避免编译,或者您认为您有一个应该起作用但没有起作用的用例?在Slack上找到我们,我们喜欢看到用例。