学习或应用一门新的编程语言时,理解这门语言所提供的性能,以及理解这些性能是否有相关联的开销,都是非常重要的环节。
这方面的问题在 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’}
}
}
枚举
当初,让咱们讨论一下枚举。
无论在 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
- 官网文档 | 缩减、混同、优化您的利用
- Jake Wharton 的博客,具体介绍了 D8 和 R8 的工作原理,并为各种性能提供了示例,以及如何间接运行编译器、如何取得反编译的后果等