此爲譯文,原文:Android CPU, Compilers, D8 & R8 – ProAndroidDevjava
設想你被分配了一項重要的太空探索任務。你須要建造一艘很是可靠的飛船。你可能會選擇普通的 YT-1300 運輸機,它很是常見,你也基本知道如何操做它。然而你老是夢想着開一個更牛逼的傢伙,你本身已經偷偷訓練了好久,事實上千年隼號纔是你真正的目標,但這個升級版的飛船要求你像 Han Solo 那樣技術嫺熟!android
最近,Google 對編譯器的改進讓我很感興趣,例如 R8 以及 Gradle 構建過程。我想是時候深刻了解而後跟大家分享一下這些改進。可是首先,讓咱們從基礎聊起。數組
當你讀完這篇文章:緩存
因此,沏上一杯咖啡,拿上光劍和小餅乾,讓咱們開始吧。安全
每個手機上,都有一個 CPU,它很是小,倒是全部運算髮生的地方。markdown
在最開始的時候,CPU 只能處理簡單的數學運算,好比加減乘除。通過這麼多年的發展,CPU 已經進化到能夠處理很是複雜的運算,好比圖片處理、音頻解碼等等。目前最知名的移動處理器是高通生產的驍龍系列。網絡
但高通並非惟一的 CPU 製造商。其餘製造商生產的 CPU 有些架構與高通相同,有些卻不同。這裏我要說:「歡迎來到地獄!」。若是你曾經開發過 C++/C,你就會知道 native code 須要爲全部支持的架構編譯一份,好比 ARM、ARM6四、X8六、X6四、MIPS 等等。架構
做爲一個 Android 開發者,一般你的應用須要支持多種多樣的設備,而這些設備背後的 CPU 架構不盡相同。這基本上意味着你須要爲每一種架構編譯一個 so 文件。說實話,這可一點都很差玩兒。不過別擔憂,我不會一直這麼抱怨 C++開發有多麼很差玩兒的。app
JVM 完美解決了這個問題。JVM 在硬件上面添加了一層抽象。經過這種方式,你的應用程序就能夠經過 Java 的接口來使用 CPU ,而你也不用去爲了避免同的 CPU 架構作適配,也不用爲了 Mac 上不同凡響的藍牙驅動而煩惱。工具
javac 編譯器將你的 Java 代碼編譯爲字節碼(.class 文件),而後你的代碼就能夠直接在 Java 虛擬機上運行,而不用關心底層操做系統的差別。做爲一個應用開發者,你不用去關心設備硬件、操做系統、內存、CPU 的差別,只須要關注業務邏輯,想法設法讓你的用戶開心就行了。
JVM 有三個主要的區域。
到這裏爲止,你已經建造了一個基本的載人飛船,耶!但它估計飛的不會太遠。讓咱們繼續深挖,看看還有那些能夠升級的選項。我想你應該準備好學習 Execution Engine 中的解釋器和 JIT 編譯器了。
這兩個傢伙在一塊兒工做,每當咱們運行咱們的程序,解釋器都須要將字節碼解釋爲機器碼再運行。這麼作最主要的一個缺點就是當一個方法須要屢次執行的時候,每次執行都須要進行解釋。想象一下,每當一個帝國士兵被克隆出來的時候,你都須要教他如何格鬥、如何握槍、如何征服一個星球,這得多痛苦啊。
JIT 編譯器就是用來解決這個問題的。執行引擎仍是使用解釋器解析代碼,但不一樣的是,當它發現有重複執行的代碼時,它會切換爲 JIT 編譯器,JIT 編譯器會將這些重複的代碼編譯爲本地機器代碼,而當一樣的方法再次被調用時,已經被編譯好的本地機器代碼就會被直接運行,從而提高系統的性能。這些重複執行的代碼也被稱爲「熱代碼(Hot code)」。
這一切又是怎麼跟 Android 關聯起來的呢?
Java 虛擬機的設計一直以來都是面向有無限電量和幾乎無限存儲的設備。
而 Android 設備則很不相同。首先電池容量有限,全部的程序都須要爲了有限的資源競爭。其次內存的大小有限,存儲空間也頗有限(跟其餘的 JVM 運行設備相比,簡直是小的可憐)。所以,當 Google 決定在移動設備上使用 JVM 的時候,他們作了不少的改動 - 包括 java 代碼編譯爲字節碼的過程以及字節碼的結構等等。下面咱們用代碼來講明這些變化。
public int method(int i1, int i2) { int i3 = i1 * i2; return i3 * 2; } 複製代碼
當這段 Java 代碼使用普通的 javac 編譯器編譯爲字節碼後,看起來大概是這樣的:
可是當咱們使用 Android 的編譯器(Dex Compiler)進行編譯時,字節碼看起來是這樣的:
之因此有這樣的區別是由於普通的 Java 字節碼是以棧爲基礎的(全部的變量都存儲在棧中),而 dex 格式的字節碼是是寄存器爲基礎的(全部的變量都存儲在寄存器中)。後者更加高效而且須要更少的空間。運行 Dex 字節碼的 Android 虛擬機被稱爲 Dalvik.
Davik 虛擬機只能加載和運行使用 Dex 編譯器生成的字節碼,與普通的 JVM 相似,也使用瞭解釋器和 JIT 編譯器。
你有沒有意識到你的飛船已經能夠在真空中飛行了呢?它得到了極大的提高,因此你須要提升本身的技能才能掌控它。確保你帶夠了小餅乾,你的大腦可能也須要一些糖分的補充。
字節碼其實就是 Java 代碼翻譯爲 JVM 可以理解的代碼。閱讀字節碼其實很是簡單,來看看這個:
每一條指令都由操做碼和寄存器(或者常量)組成。這裏有一個安卓支持的操做碼的完整列表。
與 Java 類型相同的類型:
類類型會使用完整路徑: Ljava/lang/Object;
數組類型使用 [ 前綴,後面跟着具體的類型: [I, [Ljava/lang/Object;, [[I
當一個方法有多個參數時,這些參數類型能夠直接拼接在一塊兒,咱們來練習一下:
obtainStyledAttributes(Landroid/util/AttributeSet;[III)
obtainStyledAttributes 顯然就是方法名了,Landroid/util/AttributeSet; 第一個參數就是 AttributeSet 類了,[I 第二個參數就是一個 Integer 類型的數組了,後面又連續跟着兩個 I 說明還有兩個 Integer 類型的參數。
據此能夠推斷出對應的 Java 方法聲明爲: obtainStyledAttributes(AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)
Yaay! 如今你已經掌握了基本的概念,讓咱們繼續練習:
.method swap([II)V ;swap(int[] array, int i) .registers 6 aget v0, p1, p2 ; v0=p1[p2] add-int/lit8 v1, p2, 0x1 ; v1=p2+1 aget v2, p1, v1 ; v2=p1[v1] aput v2, p1, p2 ; p1[p2]=v2 aput v0, p1, v1 ; p1[v1]=v0 return-void .end method 複製代碼
對應的 Java 代碼以下:
void swap(int array[], int i) { int temp = array[i]; array[i] = array[i+1]; array[i+1] = temp; } 複製代碼
等一下,第六個寄存器在哪兒?!眼神不錯,當一個方法是一個對象實例的一部分的時候,它有一個默認的參數 this,老是存儲在寄存器 p0 中。而若是一個方法是靜態方法的話,p0 參數有別的意義(不指代 this)。
讓咱們來看另外的一個例子。
const/16 v0, 0x8 ;int[] size 8 new-array v0, v0, [I ;v0 = new int[] fill-array-data v0, :array_12 ;fill data nop :array_12 .array-data 4 0x4 0x7 0x1 0x8 0xa 0x2 0x1 0x5 .end array-data 複製代碼
對應的 java 代碼是
int array[] = { 4, 7, 1, 8, 10, 2, 1, 5 }; 複製代碼
最後一個。
new-instance p1, Lcom/android/academy/DexExample; ;p1 = new DexExample(); invoke-direct {p1}, Lcom/android/academy/DexExample;-><init>()V ;calling to constructor: public DexExample(){ ... } const/4 v1, 0x5 ;v1=5 invoke-virtual {p1, v0, v1}, Lcom/android/academy/DexExample;->swap([II)V . ;p1.swap(v0,v1) 複製代碼
對應的 Java 代碼是
DexExample dex = new DexExample(); dex.swap(array,5); 複製代碼
如今你有了閱讀字節碼的超能力,恭喜恭喜!
在咱們開始進入 D8 和 R8 以前,咱們還須要回到 Android JVM 也就是 Dalvik 的主題上,就像是想要充理解第一部星戰,咱們須要先觀看第四部同樣,你懂的。
.java 和 .kt 代碼文件被 Java 編譯器和 Kotlin 編譯器協做編譯爲 .class 文件,這些文件又被編譯爲 .dex 文件,最終被打包進 .apk 文件。
當你從 play store 下載一個應用的時候,你下載的就是包含了全部 .dex 以及資源的 apk 安裝包,並被安裝到設備上。當你從 launcher 上點擊一個應用圖標的時候,系統就會啓動一個新的 Dalvik 進程,並將應用包含的 dex 代碼加載進來,這些代碼進一步在運行時被 Interpreter 解釋器解釋或者被 JIT 編譯器編譯。而後你就看到了應用的頁面了。
如今你已經有一個像樣的貨船了!能夠起航了!哦不對!你是一個有追求的專業飛行員,你想要的是一個更高級的宇宙飛船,那咱們就繼續來升級吧!
Dalvik 曾經是一個很不錯的解決方案,然而它也有很多侷限性。因此呢,google 後來又推出了一個優化後的 Java 虛擬機,叫 ART。ART 與 Dalvik 的主要區別是,它不是在運行時進行解釋和 JIT 編譯,而是直接運行的提早編譯好的 .oat 文件,所以得到了更好更快的運行速度。爲了提早編譯好 .oat 二進制文件,ART 使用了 AOT 編譯器(AOT 是 Ahead of Time 的縮寫)
那麼,到底什麼是 .oat 二進制文件呢?
當你從應用商店下載並安裝一個應用的時候,除了解壓縮 .apk 文件,系統也會對 .dex 文件進行編譯,生成 .oat 文件。
因此當你點擊應用圖標的時候,ART 直接加載 .oat 文件並運行,而不須要任何的解釋和 JIT 步驟。
聽起來很不錯,但看起來咱們的宇宙飛船升級的並不怎麼順利啊。
可是在銀河系中,老是會有絕地武士前來拯救世界。Google 的工程師想出了一個絕妙的點子來解決問題,充分利用了 Interpreter/JIT/AOT 的優勢。
平均來講,優化 80% 的應用代碼須要運行 8 次應用。
而後,接着是更進一步的優化 - 爲何不在相同的設備之間共享編譯選項呢?事實上,Google 就是這麼作的。
當一個設備空閒而且鏈接到一個 WIFI 網絡的時候,它會將自身的編譯 profile 經過 paly service 共享給 google,當有別的用戶使用一樣配置的的設備從 play store 下載同一個應用的時候,也會同時下載編譯 profile 用來指導 AOT 將常常運行的代碼編譯爲 .oat 存儲。這樣一來,用戶第一次運行的時候就已是優化好的應用啦。
Google 的老夥計們付出了巨大的努力來改進編譯速度,實際上咱們這些努力確實也收到了不錯的效果,然而,然而,然而,Dalvik/ART 支持的 opcodes 是很是受限的,在瞭解了前面的內容以後,你應該明白了爲何。
Java 7-8-9 等等新引入的語言特性並不能直接就能用在 Android 開發中,基本上如今的全部的 Android 開發者還在被困在 Java 6 SE 上。
爲了讓咱們能使用上 Java 8 的特性,Google 使用了 Transformation 來增長了一步編譯過程叫 desugaring,其實就是將咱們代碼裏使用的 java 8 新特性翻譯爲 Dalvik/ART 可以識別的 java 6 字節碼。這不可避免會致使一個問題 - 更長的編譯時間。
爲了解決這個問題,在 Android Studio 3.2 中,Google 使用 D8 替換了舊的 dx 編譯器。D8 的主要改進是消除 desuguaring 的過程,讓其成爲 dex 編譯的一部分,從而加快編譯速度。
能快多少呢?根據項目的不一樣表現也不同。在咱們的小項目中,編譯 100 次取平均大概會比不用 d8 快 2s.
這裏還有一個關於 D8 名字由來的趣事兒,對呀,爲何叫 D8 呢?【D 和 8 分別表明什麼呢?能讓人產生聯想的多是 Google V8 js 引擎,但並無關係】
到這裏還不是所有。
R8 是 D8 的意外收穫,他們的 codebase 是同樣的,但 R8 解決了更多的痛點。與 D8 同樣,R8 容許咱們開發使用 Java 8 的特性,並運行在老的 Davalik/ART 虛擬機中,但不只僅如此。
做爲 Android 開發的痛苦之一就是碎片化。Android 設備的種類及其龐大,上次我查看 Play Store 的時候,上面顯示有超過兩萬種設備(說到這裏羨慕的瞅一瞅只須要支持 2.5 臺設備的 iOS 開發同窗們)甚至有些廠商會修改 JIT 編譯器的工做機制。這就致使了有一部分設備行爲變得很奇怪。
R8 的對 .dex 最大的優化就是它只保留了咱們指定支持的設備所能理解的 opcodes,就像下圖所示的那樣。【這裏說的是在某些設備上某些指令會致使崩潰,看起來是會作一些處理】
Proguard 也是編譯過程當中一個 transformation 的步驟,固然也會影響編譯時間。爲了解決這個問題,R8 在 dex 過程當中也會作相似的事情(好比優化、混淆、清理無用的類),而避免了多一步 transformation.
須要注意的是,R8 並不能徹底取代 Proguard,它目前仍是一個實驗性質的只支持一部分 Proguard 功能的工具。能夠從這裏瞭解更多。
開發者喜歡 Kotlin,這門神奇的語言讓咱們能夠書寫更優雅易讀易維護的代碼,然而,Kotlin 生成的字節碼指令比對應的 Java 版本要多一些。
咱們來用 Java 8 的 lambda 語句進行一下測試:
class MathLambda { interface NumericTest { boolean computeTest(int n); } void doSomething(NumericTest numericTest) { numericTest.computeTest(10); } } private void java8ShowCase() { MathLambda math = new MathLambda(); math.doSomething((n) -> (n % 2) == 0); } 複製代碼
R8 生成的指令比 dx 生成的指令要少一些。
接下來是等價的 Kotlin 版本的代碼:
fun mathLambda() { *doSomething***{**n: Int **->**n % 2 == 0 **}** } fun doSomething(numericTest: (Int) -> Boolean) { numericTest(10) } 複製代碼
很明顯的,Kotlin 版本的指令比 Java 版本的多了很多,但 R8 也相比 dx 有更一步的優化。
對於咱們的 app 來講,跑 100 次的結果以下:
時間少了 13s,少了 1122 方法,apk 的體積也減小了 348KB,爆炸!
須要提醒你的是 R8 依然是實驗性的,Google 的工程師正在努力將它推向 Production Ready. 你想盡快開上千年隼嗎,別坐着不動了,幫咱們一塊兒加把勁兒,試試 R8,並嘗試提交一個 bug 吧!