虛擬機二三事 - Part 2. 水很深的 Android Runtime

6/22/2021 AndroidVirtual MachineJIT compilerCompiler

本篇文章將介紹 Android 虛擬機的使用,以及當你下載 google play 應用程式時,你的手機到底幫你做了什麼事情。著重在應用程式從編譯到執行的流程。

Android 每一個版本都會用一個甜點命名,好ㄘ好ㄘ。不過可惜的是,Google 宣布從Android Q 開始不再以甜品命名 QQ Android 各版本的彩蛋

# 一、 Android Runtime 的簡介

# 1-1. Overview

Android Runtime(縮寫為ART),是一種在Android作業系統上的執行環境,Android 使用虛擬機去執行 android 應用程式的 APK 文件,在 Andoird 2.2 引入 Dalvik虛擬機,在虛擬機執行的過程中,不斷利用連續的效能分析搭配及時編譯技術來最佳化程式碼的執行。而後引進的 ART 可以把應用程式的 bytecode 直接轉換成 machine code,是 android 的新的虛擬機。ART引入了AOT這種預編譯技術,在應用程式安裝的過程中,ART就已經將所有的位元組碼重新編譯成了機器碼。因此在用 google play 下載應用程式時需要事先編譯,時間才會稍微久一點。應用程式執行過程中無需進行即時的編譯工作,只需要進行直接呼叫。因此,ART極大的提高了應用程式的執行效率,同時也減少了手機的電量消耗,提高了行動裝置的續航能力,在垃圾回收等機制上也有了較大的提升。接下來就仔細介紹 Android 機制。

先附上技術變遷的時間軸,讓你知道 Android 技術經歷了四次重大變遷。

# 1-2. 為什麼 Android 需要使用虛擬機去執行程式 ?

你可能會覺得很奇怪,android OS 不是 unix-like 的作業系統嗎 ? 那為何程式不直接跑在上面就好了,還要隔一層虛擬機才能執行應用程式。參考這篇問答 (opens new window)

# 1. 回到我們使用虛擬機的初衷 - 跨平台 ( Write once, Run every where)。

現在的行動裝置硬體指令集架構的多樣性(x86, MIPS, ARM, RISC-V...),我們需要藉由虛擬機利用中間程式碼的直譯,才能達到跨平台的效果。同時他們也借鏡 Java 的精神,設計出類似的執行流程,也把 Java 當成 android 程式設計的主要語言。

# 2. Java Interface

雖然 Android OS 是使用 C 去撰寫的,但是操作作業系統的 OS API 是使用 Java 寫的,為什麼使用 Java 就如第一點所述,Google 工程師設計了很多 Android API 去使用 Java 的介面,而 Java 本身就是跑在虛擬機上。JVM 本身是一個 stack-based VM,而 Android 的 VM(稱為 Dalvik)是一個 register-based VM(這是為了生成更少的程式碼和更快的速度,以便從使用 Android 的任何設備中獲得更好的性能)。

# 1-3. Process VM

在先前的文章有提到 Process VM 的概念,簡單來說它就跟作業系統一般的 process 一樣,只是主要是讓程式語言在這個 Process 的環境下執行,所以又稱為高階程式語言虛擬機(high-level virtual machine)。

DVM 它類似於 JVM,你有 .java 文件,這些文件將被 java 編譯器編譯成 .class.class 檔案只不過是將由 JVM 運行的bytecode。JVM 可以執行在任何平台(windows、linux 或 unix)上。

在 android 中,這些文件也被編譯成 .dex 文件並由 DVM 運行。只是為了提供一個想法,當安裝應用程序時,Android 操作系統會分配唯一的 linux 用戶 ID,為每個應用程序分配一個 DVM。所以簡而言之,每個應用程序都有自己的 linux 進程、DVM 和 linux 用戶 ID。java 文件被編譯成 .dex 文件,與 .class 文件相比,它消耗的內存更少。

現在假設 10 個應用程序有 10 個單獨的 DVM,而 10 個單獨的 DVM 對於作業系統來說相當於有 10 個行程要處理。 Android OS 中的 dispatcher 或scheduler 負責處理這 10 個 Process,這就是我們有 android Activity 生命週期的原因。 您需要 DVM 來維護每個行程(每個應用程序)的執行狀態。

# 二. Android 起源 - Dalvik VM + JIT 技術 ( Android KitKat )

# 2-1. 什麼是 APK ?

除了在一般 Google Play 上下載應用程式,不知道大家有沒有自行下載過 APK 檔案安裝,有時後需要付費的 APP 被逆向工程破解後,你也可以透過 APK 檔案直接安裝免付錢的破解檔APK(爽死😎),那到底什麼是APK,根據APK 的 wiki (opens new window),一個Android應用程式的程式碼想要在Android裝置上執行,必須先進行編譯,然後被打包成為一個被Android系統的虛擬機所能辨識的檔案才可以被執行,而這種能被Android虛擬機辨識並執行的檔案格式便是「APK」。 一個APK檔案內包含被編譯的代碼檔案(.dex 檔案),檔案資源(resources), assets,憑證(certificates),和清單檔案(manifest file)。

如上圖,一個 APK 通常由許多 .dex 檔案,組合再一起,.dex 包含我們撰寫的程式碼以及使用到的函式庫,為 Bytecode,就像是 Java 要先編譯成 .class bytecode 後才能給 JVM 去直譯,.dex 檔可以被 Android VM 所直譯。Bytecode 由我們的 android runtime interpreter 翻譯成 CPU 可以懂得指令,同時 ART 也掌管記憶體和垃圾管理。

從原始碼到APK的編譯流程如下,我找了兩張圖片可以交叉參考。

流程圖一 流程圖二

1.通過AAPT(Android Assets Packing Tool)編譯資源文件,將資源文件打包編譯並生成生成R.java文件,就是放各種資源Id的那個文件。

2.通過 Java 編譯器 javac 將 java 程式碼編譯為.class bytecode

  1. 通過Dalvik編譯器.class轉化為.dex

4.通過 Apk builder 將打包後的資源與.dex文件一起生成APK文件

# 2-2. 用時間換空間的 Dalvik VM

早期的智慧型手機硬體資源十分有限,記憶體容量都很小(256MB或更小),為了最佳化記憶體的使用率,Dalvik VM 就隨之出生了,直譯時搭配及時編譯技術(Just in time compilation),因為早期的 APP 規模較小,所以在做及時編譯技術時雖然會暫存熱點機器碼在記憶體,但是比起整個程式先編譯好再直接放入記憶體,直譯器邊翻譯邊執行再加上及時編譯技術犧牲少量的記憶體成本,換取更大的執行效率以及記憶體空間。但其實相當有限,因為早期記憶體容量比較小。

下圖為 Dalvik VM 啟動 APP 的流程

所以在 Android KitKat 的整體流程如下圖

# 2-3. Android KitKat 的優缺點

  • 優點

    • 安裝速度超快

      因為下載後程式碼不用編譯,直接用虛擬機執行 bytecode 就好。

    • 儲存空間小

      同樣的,下載後不需要編譯,直接執行載下來的 bytecode 就好。

  • 缺點

    • dex 載入的時候會非常慢

      因為在dex加載時會進行 dexopt,把 dex 檔案最佳化成 odex檔

    • JIT compilation Overhead

      因為後期的 APP 規模逐漸成長,JIT 監控的負擔也越來越重,JIT 編譯時有時也會停頓。

    • Battery Overhead

      因為 JIT 的 monitor 要一直監控,每次重新啟動都要重新監控、重新JIT編譯,花費很多 CPU 資源,對於手機來說非常耗電。

    • 首次啟動執行很慢

      因為在程式退出後重新執行時,剛剛JIT得到的熱點程式碼訊息都消失了,少了熱點程式碼的快取,除了執行時會比較慢,再來是 JIT 又要重新監控 = =

而後由於儲存裝置技術的進步,儲存空間變得非常寬裕,因此在 Android L後,就以嶄新的虛擬機 Android Runtime (ART) 來取代 Dalvik VM。

# 三. 以空間換時間: 全新 ART 的時代 - 加入 AOT 技術 ( Android Lollipop )

# 3-1. AOT 介紹

以下圖當作本小節的開頭,DVM 使用 JIT 方式編譯,ART 使用 AOT 方式編譯。

ART 在 Android L 中的工作方式與我們從 Dalvik 所知道的相比發生了 180 度的變化。ART 並沒有像在 Dalvik 中那樣使用及時編譯,而是使用了一種稱為提前編譯(AOT- Ahead of time compilation)的策略。這種方法極大地提高了執行時的效能,比及時編譯快 20 倍。ART是在 App 安裝的時候將 .apk 文件解壓,並將 .dex 預編譯為 .oat 可執行文件,當 App 啟動的時候就不需要在Runtime直譯執行了。

所以在 Android KitKat 的整體流程如下圖

# 3-2. Android Lollipop 優缺點

  • 優點

    • 執行時速度變快

      ART 在下載時,已經預先編譯好了,因為已經是機器碼,不需要重新翻譯,執行時效率大幅提升。

    • 執行時十分省電

      因為不需要 JIT 的監控

  • 缺點

    • App 的安裝、更新時間變長

      因為 App 在安裝或更新的時候,都要進行 dex2oat 的動作,將 dex 進行預編譯,所以時間會較久。

    • 佔用空間太大

      不論是常用的、不常用的程式碼,都被編譯成一個二進位檔案,導致程式相當肥大,占用儲存設備大,同時也更吃記憶體。

    • 捨棄 JIT - 靜態編譯成本

      動態編譯技術可以在執行期獲得靜態編譯所沒有的熱點程式碼的資訊,來做效能最佳化。因此如果有些使用者不常用的程式碼被靜態編譯,浪費了編譯它的時間,也會讓這塊不常用的程式碼常駐在記憶體。

聰明的 Google 工程師,當然要改善這個大缺點,能省則省、能用就用。在 Android Nougat 版本,將會使用 profile-guided compilation 的技術搭配及時編譯技術,同時結合動態編譯和靜態編譯的優點。

# 四. 摸斗摸斗嗨壓苦 - Profile-guided compilation : AOT + JIT ( Android Nougat )

# 4-1. Profile-guided compilation 介紹

我選了三張圖,可以先參考一下。

  1. 當你首次安裝並一個App的時候,AOT 不將.dex文件編譯為.oat文件,這一步減少了安裝時間,系統通過Interpreter的方式來啟動App。

  2. 當在App運行過程中探測到 hot code,就使用JIT編譯,這些通過JIT編譯的平台程式碼會被存儲在快取或記憶體中(JIT code cache),加快下次執行的速度,並產生 profile 文件(編譯配置文件,記錄熱點函數訊息)儲存。

  3. 當設備處於空閒時間(IDLE)或充電狀態(charging)時,系統每隔一段時間就會去掃描 APP 目錄下 profile 文件(編譯所使用的Daemon Service,背景執行),並啟動 AOT 編譯器(在這裡官方稱為 profile-guided compilation),結合編譯配置文件將熱點代碼相關資訊編譯為 .oat 可執行文件。當再次執行App時,ART 就可以直接執行.oat文件了。

因為是用 JIT 產生的 profile 再引導 AOT 編譯,所以才稱為 profile-guided compilation。

這個方法有幾個特點要注意

  1. JIT的過程中,生成 Offline 的 Profile
  2. 系統守護行程服務,再背景監控,一旦在充電或是空閒,就會進行 AOT 編譯 profile
  3. 每個APP都會有自己的Profile文件,保存在App本身的Local Storage中

整體的 Workflow 如下

# 4-2. Android Nougat 的優缺點

  • 優點

    • 更聰明、精準的編譯方式

    以往靜態編譯最佳化時,打不到(最佳化不到)真正的痛點,現在先使用 JIT 來探測一下熱點程式碼再引導 AOT 靜態編譯,節省更多空間,同時又可以高效率執行。

    • 安裝、更新速度提升

    AOT 方式的壞處是下載後要先編譯,造成每次程式自己更新時會變慢,手機會變熱,現在下載後不用經過編譯的階段。

  • 唯一的缺點

    因為最一開始使用仍要經過 JIT,所以前幾次執行時會比較慢。

在時間跟空間完美的協調之後,仍然有一個汙點,Google 工程師眼裡揉不得沙子,生出了更進一步改善方法。

# 五. 最後的大雜交 - Profile on the cloud ( Android Pie )

雲端配置文件(Profile on the cloud) 背後的 主要思想是大多數人以非常相似的方式使用該應用程式。因此,為了在安裝後立即提高性能,我們可以從已經使用過此應用程序的人那裡收集個人資料數據。此聚合配置文件資料用於為應用程序創建一個稱為共同核心配置文件(common core profile)的文件。

總而言之,就是蒐集使用者的資料,上傳每一個使用者的 profile,之後雲端會生成一個共同核心配置文件,共同核心配置文件相當於使用者的共同行為、常使用哪些功能的資訊,因此,當新用戶安裝該應用程序時,該文件將與該應用程序一起下載。ART 使用它來預編譯大多數用戶經常執行的類別和方法,這樣,新用戶在下載應用程序後立即獲得更好的性能。

由下圖可以看到啟動時間有了大幅的提升。

這方法提升啟動速度,ART 之後也會隨著使用者各自的行為,再對該使用者最常用的程式碼進行編譯。

# 六、總結

引入開頭的一張圖當作總結

Android 版本 特性 優點 缺點
Android KitKat Android Runtime 使用 Dalvik VM 實現,搭配 JIT 及時編譯 省儲存空間跟記憶體空間 (對於早期的行動裝置)
  1. 對於規模逐漸龐大的 APP,JIT 負擔變大,執行時有時候會變慢
  2. JIT monitor 要一直監控,手機會變熱且很耗電
Android Lollip Android Runtime 引入使用 AOT 編譯的 ART
  1. 執行速度變更快(約20倍)
  2. 更加省電,因為少了 JIT 一直監控
  1. 因為要預編譯,更長的安裝、更新時間
  2. 編譯後的檔案較大,使用更多的 RAM 跟 ROM
Android Nougat Profile-guided compilation ( JIT + AOT )
  1. 使程式下載安裝、更新時不用預編譯,安裝更新速度變快
  2. 最佳化程式碼時更加精準(Profile-guided,符合使用者行為)
  3. 程式檔案較小
剛開始使用很慢,因為要先請 JIT 幫你看你常用哪個區段
Android Pie Profile in the Cloud 集合不同使用者的 profile 成 common core profile,之後下載後先編譯這段大家常用的程式碼,可以加速啟動的速度。 下載時要多花一些時間,程式下載時也多下載 common core profile,安裝要等一下下因為要編譯 profile,程式也稍微變大一些,但是我認為已經做出了最佳的取捨了 😉

最後以 DVM 和 ART 的從原始碼的流程圖,結束本篇文章。

# 七、參考連結

  1. Android Runtime — How Dalvik and ART work?(必看) (opens new window) + 搭配正妹講解影片 (opens new window)

  2. 深入理解Android虛擬機及編譯系統 (opens new window)

  3. Android虛擬機的JIT編譯器 (opens new window)

  4. Android: Dalvik vs ART (opens new window)

  5. 深入理解 Android(二):Java 虚拟机 Dalvik (opens new window)

  6. Dalvik和ART編譯方式的演進 (opens new window)

  7. JVM、Dalvik、ART 介紹 (opens new window)

  8. How to implement a simple dalvik virtual machine (opens new window)

  9. 什麼是Dalvik Virtual Machine? (opens new window)

  10. Android 的 ART 是什麼東西,有何作用? (opens new window)

  11. 繼續談談下一代Android VM runtime: ART (opens new window)

  12. Working of Dalvik Virtual Machine in Android (opens new window)

  13. Why does Android need a Virtual Machine(DVM)? (opens new window)

  14. Android上的Dalvik虛擬機 (opens new window)

  15. APK 的 wiki (opens new window)

Last Updated: Tue Jul 06 2021 19:28:11 GMT+0800