Kotlin Vocabulary | 枚举和 R8 编译器

335 阅读5分钟
原文链接: mp.weixin.qq.com

学习或使用一门新的编程语言时,了解这门语言所提供的功能,以及了解这些功能是否有相关联的开销,都是十分重要的环节。 这方面的问题在 Kotlin 中显得更加有趣,因为 Kotlin 最终会编译为 Java 字节码,但是它却提供了 Java 所没有的功能。那么 Kotlin 是怎么做到的呢?这些功能有没有额外开销?如果有,我们能做些什么来优化它吗? 接下来的内容与 Kotlin 中枚举 (enums) 和 when 语句 (java 中的 switch 语句) 有关。我会讨论一些和 when 语句相关的潜在开销,以及 Android R8 编译器是如何优化您的应用并减少这些开销的。

编译器

首先,我们讲一讲 D8 和 R8。 事实上,有三个编译器参与了 Android 应用中 Kotlin 代码的编译。

1. Kotlin 编译器

Kotlin 编译器将会首先运行,它会把您写的代码转换为 Java 字节码。虽然听起来很棒,但可惜的是 Android 设备上并不运行 Java 字节码,而是被称为 DEX 的 Dalvik 可执行文件。Dalvik 是 Android 最初所使用的运行时。而 Android 现在的运行时,则是从 Android 5.0 Lollipop 开始使用的 ART (Android Runtime),不过 ART 依然在运行 DEX 代码 (如果替换后的运行时无法运行原有的可执行文件的话,就毫无兼容性可言了)。

2. D8

D8 是整个链条中的第二个编译器,它把 Java 字节码转换为 DEX 代码。到了这一步,您已经有了能够运行在 Android 中的代码。不过,您也可以选择继续使用第三个编译器 —— R8 。

3. R8 (可选,但推荐使用)

R8 以前是用来优化和缩减应用体积的,它基本上就是 ProGuard 的一个替代方案。R8 不是默认开启的,如果您希望使用它 (例如您想要这里讨论到的那些优化时),就需要启用它。在模块的 build.gradle 里添加 minifyEnabled = true ,就可以强制打开 R8 。它将在所有其他编译工作后执行,来保证您获得的是一个缩减和优化过的应用。
 android {    buildTypes {        release {            minifyEnabled true             proguardFiles getDefaultProguardFile(                ‘proguard-android-optimize.txt’),                ‘proguard-rules.pro’        }    }}
  • D8

    https://developer.android.google.cn/studio/command-line/d8

  • R8

    https://developer.android.google.cn/studio/build/shrink-code

枚举

现在,让我们讨论一下枚举。 无论在 Java 还是 Kotlin 中,枚举的功能和消耗本质上都是一样的。有趣的地方在于引入了 R8 之后,我们能对其中的一些开销做些什么。 枚举本身不包含任何隐藏开销。使用 Kotlin 时,也仅仅是将其转换为 Java 编程语言中的枚举而已,并没有多大开销。(我们曾经提到避免使用枚举,但那是很多年前的事了,而且运行时也与今日不同。所以现在使用枚举没什么问题。) 但当您配合枚举使用 when 语句时,就会引入额外的开销。 首先,我们来看一个枚举的示例:
enum class BlendMode {    OPAQUE,    TRANSPARENT,    FADE,    ADD}

这个枚举中包含四个值。这些值是什么无关紧要,这里仅作为示例。

枚举 + when

接下来,我们使用一个 when 语句来转换这个枚举:

fun blend(b: BlendMode) {    when (b) {        BlendMode.OPAQUE -> src()        BlendMode.TRANSPARENT -> srcOver()        BlendMode.FADE -> srcOver()        BlendMode.ADD -> add()    }}

对应枚举的每一个值,我们都去调用另一个方法。

如果您去看这段代码编译成的 Java 字节码 (您可以通过 Android Studio 的查看字节码功能直接看到 (Tools -> Kotlin -> Show Kotlin Bytecode),然后点击 "Decompile" 按钮),就会看到下面这样的代码:

public static void blend(@NotNull BlendMode b) {    switch (BlendingKt$WhenMappings.            $EnumSwitchMapping$0[b.ordinal()]) {        case 1: {            src();            break;        }        // ...    }}

这段代码中没有对枚举直接使用 switch 语句,而是调用了一个数组。这个数组是从哪来的呢?

而且这个数组存储在一个被生成的类文件中。这个类文件是从哪来的?

这里究竟发生了什么呢?

自动生成的枚举映射

事实上,为了实现二进制兼容,我们不能简单地依靠枚举的序数值进行转换,因为这样的代码十分脆弱。假设您的一个库中包含了一个枚举,而您改变了这个枚举中值的顺序,您就可能破坏了某个人的应用。虽然这些代码除了顺序,看起来完全相同,但就是这种顺序的不同导致了对其它代码的影响。

所以取而代之的是,编译器将序数值与另一个值做映射,这样一来,无论您对这些枚举做什么修改,基于这个库的代码都能正常运行。 当然,这就意味着只要像这样使用枚举,就会额外生成其它内容。在本例中,就会生成很多代码。 生成的代码就像下面这样:
public final class BlendingKt$WhenMappings {    public static final int[] $EnumSwitchMapping$0 =            new int[BlendMode.values().length];    static {        $EnumSwitchMapping$0[BlendMode.OPAQUE.ordinal()] = 1;        $EnumSwitchMapping$0[BlendMode.TRANSPARENT.ordinal()] = 2;        $EnumSwitchMapping$0[BlendMode.FADE.ordinal()] = 3;        $EnumSwitchMapping$0[BlendMode.ADD.ordinal()] = 4;    }}

这段代码中生成了一个 BlendingKt$WhenMappings 类。这个类里面有一个存储映射信息的数组: $EnumSwitchMapping$0,接下来则是一些执行映射操作的静态代码。

示例中是只有一个 when 语句时的情况。但如果我们写了更多的 when 语句,每个 when 语句就会生成一个对应的数组,即使这些 when 语句都在使用同一个枚举也一样。

虽然所有这些开销没什么大不了的,但是却也意味着,在您不知情的时候,会生成一个类,而且其中还包含了一些数组,这些都会让类加载和实例化消耗更多的时间。

幸运的是,我们可以做一些事情来减少开销: 这就是 R8 发挥作用的时候了。

使用 R8 来解决问题

R8 是一个有趣的优化器,它能 "看" 到与应用相关的所有内容。由于 R8 可以 "看" 到无论是您自己写的还是您依赖的库中的所有代码,它便可以根据这些信息决定做哪些优化。比如,它能避免枚举映射造成的开销: 它不需要那些映射信息,因为它知道这些代码只会以既定的方式使用这些枚举,所以它可以直接调用序数值。 下面是 R8 优化过的代码反编译后的样子:
public static void blend(@NotNull BlendMode b) {    switch (b.ordinal()) {        case 0: {            src();            break;        }        // ...    }}

这样就避免了生成类和映射数组,而且只创建了您所需的最佳代码。

探索 R8 与 Kotlin,然后用 Kotlin 写出更好的应用吧。

更多信息

更多 R8 相关信息,请查看以下资源:
  • 官方文档 | D8 https://developer.android.google.cn/studio/command-line/d8
  • 官方文档 | 缩减、混淆、优化您的应用 https://developer.android.google.cn/studio/build/shrink-code
  • Jake Wharton 的博客,详细介绍了 D8 和 R8 的工作原理,并为各种功能提供了示例,以及如何直接运行编译器、如何获得反编译的结果等 https://jakewharton.com/blog/


推荐阅读

 点击屏末    |  查看 Android 开发者文档《缩减、混淆处理和优化您的应用》