Kotlin Vocabulary | 枚舉和 R8 編譯器

學習或使用一門新的編程語言時,了解這門語言所提供的功能,以及了解這些功能是否有相關聯的開銷,都是十分重要的環節。

這方面的問題在 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 相關信息,請查看以下資源:

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,996評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374