Faster builds: non-transitive R files

background

在 AGP 4.2 中,所有模块都可以使用非传递 R 文件。非传递性 R 文件使您的构建速度越来越快,并且您的 AAB/APK 更小。
复制代码
如果 A 依赖于 B 并且 B 依赖于 C,A 不知道 C。在 Android 的世界中,非传递 R 文件:
复制代码
非传递 R 类启用每个库的 R 类的命名空间,以便其 R 类仅包含在库本身中声明的资源,而不包含库的依赖项中的资源,从而减少该库的 R 类的大小。
复制代码

The past and future of the Android R class

每个 Android 应用程序都包含一些资源,例如本地化字符串、图标、屏幕布局或导航目标。每个原生 Android 应用程序都使用基于 R 类中列出的资源 ID 的 Android 检索机制来访问这些资源。
复制代码

What are R classes?

Android 应用程序资源位于 res 根目录下的目录结构中(不要与 Java 应用程序的资源目录混淆),但格式可能不同。图标和布局放置在各自的文件中,但字符串可能(或可能不)全部存储在单个文件中,导航目标是导航图更丰富内容的一部分。
复制代码
尽管如此,所有应用程序资源都有一个唯一的资源 ID,由 aapt 工具在编译期间生成。所有资源 ID 都列在具有简单名称的 Java 类中 - R。对于每种资源类型,都有一个具有资源类型名称的嵌套类(例如,R.string 表示字符串资源),并且对于该类型的每种资源,都有一个静态整数(例如,R.string.app_name)。
复制代码
这种资源检索机制在 Android SDK 的许多部分中大量使用,并针对性能进行了优化。不幸的是,它并未针对开发人员的幸福和安全进行优化。由于所有资源 ID 都是 Int 类型,因此可以轻松地使用字符串资源 ID 来检索最终不会很好的图标。
复制代码

origin

自 2008 年首次公开发布 Android 以来,资源检索的原则一直保持不变。在编译应用程序代码之前,必须找到 res 目录下的所有资源,然后生成 R 类源代码并与其余应用程序源代码合并。完成后,应用程序代码可以像任何其他代码一样引用 R 类的资源 ID。
复制代码
当时,Android Development Tools (ADT) Plugin负责启动aapt,生成R.java源文件,与其他应用程序源文件合并,一起编译。
复制代码

image.png

appt 的第一个版本将 R 类中的所有资源 ID 生成为常量(Java 中的 public static final int)。这对于具有多个库模块的模块化项目的构建性能似乎非常不利。这些模块中资源 ID 的实际值可能会发生冲突,因此所有库模块都必须更频繁地重新编译。因此,从 ADT 版本 14 开始,库模块的 R 类将其字段生成为 public static int(非 final),但叶应用程序模块将其字段保持为 final,因为没有其他模块依赖它们。
复制代码
这种变化在当时是一个很大的问题,因为 Java 的 switch 语句需要编译时常量,并且不能使用库模块资源 ID 作为语句值。但是对于 Kotlin 的 when 表达式,这不再是一个问题。
复制代码

Gradle era

在 2014 年的 Google I/O 上,具有基于 Gradle 的构建系统的 Android Studio 被宣布作为 ADT 和基于 Ant 的构建的替代品。但由于 R 类代码生成是由 aapt 工具完成的,因此资源 ID 的生成和使用方式并没有太大变化。
复制代码

Elephant is building

随着项目的增长,他们的 R 类也在增长。大约在 2017 年,一些更大的开发团队意识到 R 类的增长速度要快得多。 Elin Nilsson 在她关于应用程序模块化的演讲中提到,在他们的 120 万个 LoC 代码库中,生成的 R 类加起来总计有 5600 万个 LoC,需要在每次 clean 构建时编译和索引。
复制代码
R 类如此庞大的主要原因是它们包含重复项。为构建的每个模块生成 R 类,并且特定于模块的 R 类包括对其传递依赖项的所有资源的引用。
复制代码

image.png

在此示例中,MDC 库包含 com.google.android.material.R 类,其中包含对材料资源的引用。
复制代码
我们的 :lib 模块依赖于 MDC 库并且包含很少的其他资源。它还有自己的 com.example.myapp.lib.R 类,其中列出了对其自身资源的引用和对 MDC 库资源的传递引用。
复制代码
最后,:app 模块有自己生成的 com.example.myapp.R 类,并再次列出 :lib 模块和 MDC 库引用 ID。
复制代码
Material Components 库资源 ID 生成 3 次!这就是一个小型模块化应用程序如何在 R 类中实现数百万 LoC 的故事。
复制代码

rescue

Android Gradle 插件 3.3(2019 年 1 月)引入了一个记录不充分的标志,您可以在 gradle.properties 中启用:

android.namespacedRClass=true
复制代码
它支持非传递 R 类命名空间,其中每个库仅包含对其自身资源的引用,而不从依赖项中提取引用。这对 R 类的大小有很大的影响,从而导致构建速度更快。
复制代码
它使模块从架构的角度更好地隔离。除非从另一个模块显式导入 R 类,否则该模块只能使用自己的资源。它可以防止模块意外使用来自另一个模块的 drawable 或字符串。
复制代码
不幸的是,Android Studio 并没有意识到这一点,因此它不会阻止您进行错误的引用,但至少它会在编译时失败。
复制代码
但归根结底,R 类只是编译时已知的 Int 列表,您仍然需要编译它。如果 AGP 可以直接生成 Java 字节码不是很好吗?另一个 gradle.properties 标志实现了这个梦想:

android.enableSeparateRClassCompilation=true
复制代码
这将使 AGP 将 R 类生成为已编译的 jar 文件而不是源代码,并自动将它们与项目的其余部分合并。从技术上讲,构建过程将在 build/intermediates 目录中生成 R.jar 文件,而不是 build/generated 目录中的 R.java 文件。
复制代码
在 Android Gradle 插件版本 3.4 上,您可以通过在 gradle.properties 文件中添加以下行来选择检查您的项目是否声明了可接受的包名称:

android.uniquePackageNames=true
复制代码
当时还没有这个要求的细节,但是一个包名只在一个项目模块中使用似乎是一件好事,所以为什么不现在启用这个检查并且将来没有迁移问题,对吧?
复制代码
Android Gradle 插件 3.6(2020 年 2 月)为每个模块提供唯一包名称的要求提供了答案。从这个版本开始,AGP 通过为每个模块只生成一个 R 类来加速编译,从而简化了编译类路径。
复制代码
AGP 3.6 还使 android.enableSeparateRClassCompilation 标志默认启用并且不能再禁用。
复制代码
另一方面,引入了新的 android.enableAppCompileTimeRClass 实验标志。它仅限于 Android 应用程序模块。在应用程序模块的编译阶段开始之前,所有其他模块的所有 R 类都需要重新生成,以创建最终的一组唯一资源 ID,这些资源 ID 将在运行时在应用程序模块中使用。这个新的实验标志通过预先生成一个假的应用程序模块 R 类,然后用真实的资源 ID 值更新它来解决这个限制。这种方法的一个限制是应用程序模块的资源 ID 不再是最终的(与库模块中相同),因此它们不能用于 Java 的 switch 语句或作为注释参数。
复制代码
Android Gradle 插件 4.1(2020 年 10 月发布候选版本)又迈出了一步,使 R 类命名空间默认启用,因为它将标志从:

android.namespacedRClass=true

转换为:

android.nonTransitiveRClass=true
复制代码
应用程序模块也有类似的标志:

android.experimental.nonTransitiveAppRClass=true
复制代码
但是根据 AGP 源代码中的注释,这似乎只是暂时的,将来会被删除,所以你现在不必费心了。
复制代码

non-transitive modules

如果模块 A 依赖于 B,B 依赖于 C。我们如何引用资源?让我们来看看:
复制代码
模块 A 可以引用它自己的资源(和正常一样):

R.string.hello_world
复制代码
模块 A 可以引用模块 B 资源(完全合格的包):

com.my.moduleB.R.string.hello_neighbour
复制代码
模块 A 不能引用模块 C 的资源。
复制代码
可以使用带有 Android Gradle 插件 (>4.2) 的非传递 R 类来为具有多个模块的应用程序构建更快的构建。 ......这导致更多 up-to-date 构建和编译避免的好处。
复制代码

Benefits of non-transitive R classes?

减少 AAB/APK 大小,包括 DEX 字段引用计数
减少增量构建速度,因为在进行更改时可以包含更少的依赖项
模块化增加,依赖关系变得更加明确
降低复杂性,资源不能来自传递依赖
随着包含的代码减少,整个构建持续时间减少
复制代码
可以通过编辑 /gradle.properties 文件来为自己打开此设置,以包括:

android.nonTransitiveRClass=true
复制代码

image.png

这种自动重构不是灵丹妙药,它可能会导致一些资源引用错误,即它会在 R 类之前添加错误的包名,或者根本无法选择一个,或者它会添加一个你的模块没有依赖。根据我的经验,这些问题是在构建时发现的,您可以从以下三个解决方案步骤中进行选择,以完成重构工具的启动:
复制代码
完成后,您将必须构建项目并修复错误。错误可能有几种格式:
复制代码
使用来自另一个模块的资源。
修复:添加完全限定的包,或导入 R 文件,或(仅限 Kotlin)使用别名。
复制代码
// Fully qualifed package\
val foo = com.my.moduleB.R.string.hello_neighbour\
// Import then use string.hello_neighbour\
import com.my.moduleB.R\
// Alias then use RB.string.hello_neighbour\
import com.my.moduleB.R as RB
复制代码
使用来自另一个模块的资源,但您没有声明为对该模块的依赖。
修复:像 #1 一样做,但也在 gradle 中添加依赖项。
复制代码
implementation project(":libraries:moduleB")
复制代码
使用来自另一个模块的资源,但您没有声明对该模块的依赖关系,并且您不想声明依赖关系。
修复:这里的解决方案是生成引用并使用相同的名称,或者复制/创建您想要的新资源。
复制代码

image.png

Guess you like

Origin juejin.im/post/7083387821334986760