Kotlin Vocabulary | 枚举和 R8 编译器

学习或使用一门新的编程语言时,了解这门语言所提供的功能,以及了解这些功能是否有相关联的开销,都是十分重要的环节。java

这方面的问题在 Kotlin 中显得更加有趣,由于 Kotlin 最终会编译为 Java 字节码,可是它却提供了 Java 所没有的功能。那么 Kotlin 是怎么作到的呢?这些功能有没有额外开销?若是有,咱们能作些什么来优化它吗?android

接下来的内容与 Kotlin 中枚举 (enums) 和 when 语句 (java 中的 switch 语句) 有关。我会讨论一些和 when 语句相关的潜在开销,以及 Android R8 编译器是如何优化您的应用并减小这些开销的。编程

编译器

首先,咱们讲一讲 D8 和 R8。数组

事实上,有三个编译器参与了 Android 应用中 Kotlin 代码的编译。app

1. Kotlin 编译器编程语言

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

2. D8gradle

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

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

R8 之前是用来优化和缩减应用体积的,它基本上就是 ProGuard 的一个替代方案。R8 不是默认开启的,若是您但愿使用它 (例如您想要这里讨论到的那些优化时),就须要启用它。在模块的 build.gradle 里添加 minifyEnabled = true ,就能够强制打开 R8 。它将在全部其余编译工做后执行,来保证您得到的是一个缩减和优化过的应用。

android {
    buildTypes {
        release {
            minifyEnabled true
 
            proguardFiles getDefaultProguardFile(
                ‘proguard-android-optimize.txt’),
                ‘proguard-rules.pro’
        }
    }
}

枚举

如今,让咱们讨论一下枚举。

不管在 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 相关信息,请查看如下资源:

相关文章
相关标签/搜索