這方面的問題在 Kotlin 中顯得更加有趣,由於 Kotlin 最終會編譯爲 Java 字節碼,可是它卻提供了 Java 所沒有的功能。那麼 Kotlin 是怎麼作到的呢?這些功能有沒有額外開銷?若是有,咱們能作些什麼來優化它嗎?java
接下來的內容與 Kotlin 中枚舉 (enums) 和 when 語句 (java 中的 switch 語句) 有關。我會討論一些和 when 語句相關的潛在開銷,以及 Android R8 編譯器是如何優化您的應用並減小這些開銷的。android
首先,咱們講一講 D8 和 R8。編程
事實上,有三個編譯器參與了 Android 應用中 Kotlin 代碼的編譯。數組
1. Kotlin 編譯器bash
Kotlin 編譯器將會首先運行,它會把您寫的代碼轉換爲 Java 字節碼。雖然聽起來很棒,但惋惜的是 Android 設備上並不運行 Java 字節碼,而是被稱爲 DEX 的 Dalvik 可執行文件。Dalvik 是 Android 最初所使用的運行時。而 Android 如今的運行時,則是從 Android 5.0 Lollipop 開始使用的 ART (Android Runtime),不過 ART 依然在運行 DEX 代碼 (若是替換後的運行時沒法運行原有的可執行文件的話,就毫無兼容性可言了)。app
2. D8編程語言
D8 是整個鏈條中的第二個編譯器,它把 Java 字節碼轉換爲 DEX 代碼。到了這一步,您已經有了可以運行在 Android 中的代碼。不過,您也能夠選擇繼續使用第三個編譯器 —— R8。學習
3. R8 (可選,但推薦使用)gradle
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 語句來轉換這個枚舉:
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 優化過的代碼反編譯後的樣子:
public static void blend(@NotNull BlendMode b) {
switch (b.ordinal()) {
case 0: {
src();
break;
}
// ...
}
}
複製代碼
這樣就避免了生成類和映射數組,並且只建立了您所需的最佳代碼。
探索 R8 與 Kotlin,而後用 Kotlin 寫出更好的應用吧。
更多 R8 相關信息,請查看如下資源:
developer.android.google.cn/studio/comm…
developer.android.google.cn/studio/buil…