虛擬機運行 Android 程序背後的故事

衆所周知,Android 最開始面世時,使用的開發語言是 Java,而 Java 是運行在 Java 虛擬機上的,即 JVM。那麼爲什麼 Google 要單獨設計一套新的 Dalvik 虛擬機來執行 Android 程序呢?可能是爲了解決移動端設備上軟件運行效率問題,可能是 JVM 虛擬機無法滿足當時移動端的使用場景,也可能是爲了規避與 Oracle 公司的版權糾紛問題,最終 Google 專門爲 Android 平臺設計了一套虛擬機來運行 Android 程序,它就是 Dalvik Virtual Machine(Dalvik 虛擬機)。

隨着 Android 發展至今,雖然目前開發 Android 程序的語言已經越來越多樣性,如 Java,Kotlin,Dart,Flutter 等等,但無論使用哪種語言開發 Android,最終都需要運行在虛擬機上,本篇文章將站在 Android 虛擬機的視角來分析 Android 程序的運行原理。

1、Dalvik 虛擬機概述及特點

Google 於 2007 年底正式發佈了 Android SDK, Dalvik 虛擬機也正式進入我們的視野,而 Dalvik 命名的由來是取至其作者**丹·伯恩斯坦(Dan Bornstein)**曾居住過一個名叫 Dalvik 的小漁村。Dalvik 虛擬機作爲 Android 平臺的核心組件,擁有如下幾個特點:

  • 體積小,佔用內存空間小;
  • 專有的 DEX 可執行文件格式,體積更小,執行效率更快;
  • 常量池採用 32 位索引值,尋址類方法名、字段名、常量更快;
  • 基於寄存器架構,並擁有一套完整的的指令系統;
  • 提供了對象生命週期管理、堆棧管理、線程管理、安全和異常管理以及垃圾回收等重要功能;
  • 所有的 Android 程序都運行在 Android 系統進程裏,每個進程對應着一個 Dalvik 虛擬機實例;

2、Dalvik 虛擬機與 Java 虛擬機的區別

從 Dalvik 虛擬機的特點我們可以看出 Dalvik VM 和 JVM 還是有許多的不同點的,兩者並不兼容,他們顯著的不同點主要有以下幾個方面:

2.1、Java 虛擬機運行的是 Java 字節碼,Dalvik 虛擬機運行的是 Dalvik 字節碼

傳統的 Java 程序經過編譯,生成 Java 字節碼保存在 .class 文件中,Java 虛擬機通過解碼 .class 文件中的內容來運行程序。而 Dalvik 虛擬機運行的是 Dalvik 字節碼,所有的 Dalvik 字節碼由 Java 字節碼轉換而來,並被打包到一個 DEX(Dalvik Executable) 的執行的文件中,Dalvik 虛擬機通過解釋 DEX 文件來執行這些字節碼。

2.2、Dalvik 可執行文件的體積更小

Android SDK 中有一個叫 dx 的工具負責將 Java 字節碼轉換成 Dalvik 字節碼。dx 工具對 Java 類文件重新排列,消除在類文件中出現的所有冗餘的信息,避免虛擬機在初始化時出現重複的文件加載與解析過程。

一般情況下,Java類文件中包含多個不同的方法簽名,如果其他的類文件引用該類文件中的方法,方法簽名也會被複制到其類文件中,也就是說,多個不同的類會同時包含相同的方法簽名,同樣的,大量的字符串常量在多個類文件中也被重複使用,這些冗餘信息會直接增加文件的體積,同時也會嚴重影響虛擬機解析文件的效率。dx 工具針對這個問題專門做了處理,它將所有的 Java 類文件中的常量池進行分解,消除其中的冗餘信息,重新組合形成一個常量池,所有的類文件共享一個常量池。

由於 dx 工具對常量池的壓縮,使得相同的字符串、常量在 DEX 文件中只出現一次,從而減少了文件的體積。其轉換過程可參考下圖:
在這裏插入圖片描述

2.3、Dalvik 虛擬機與 Java 虛擬機架構不同

Java 虛擬機基於棧結構。Java 程序在運行時虛擬機需要頻繁的從棧上讀取或寫入數據,這個過程需要更多的指令分派與內存訪問次數,會消耗不少 CPU 執行時間,對與像手機設備資源有限的設備來說,這是相當大的一筆開銷了。
Dalvik 虛擬機是基於存儲器架構。數據的訪問通過多個存儲器之間直接傳遞,這樣的訪問方式比基於棧的方式要快得多。深入瞭解請移步 Dalvik 虛擬機和 Sun JVM 在架構和執行方面有什麼本質區別

3、Dalvik 虛擬機是如何執行程序的

對 Dalvik 有初步瞭解之後,要弄明白 Dalvik 虛擬機是如何執行程序的我們可以把這個問題拆分成以下幾個問題

  • 1、Dalvik 在整個 Android 系統中所處的角色?
  • 2、Dalvik 是如何被創建且初始化的?
  • 3、Dalvik 的可執行文件 dex 與 apk 之間的關係?
  • 4、Dalvik 是如何加載並執行 dex 文件的?

接下來我們按照這四個問題,來逐步揭開 Dalvik 的面紗。

3.1、Dalvik 在整個 Android 系統中所處的角色?

首先我們通過官網一張圖來了解 Dalvik 虛擬機在整個 Android 架構中所處的位置:

在這裏插入圖片描述
細心的同學乍一看這張圖會,發現並沒有找到 Dalvik 的位置,這是因爲 Android 版本 5.0 之後,Android 逐漸使用 ART 來代替 Dalvik ,他們在整個 Android 系統中充當的角色本質是一樣的,沒有被改變,它們屬於 Android 運行時環境,與一些核心庫共同承擔着 Android 應用程序的運行工作。

暫時可以理解成 ART 就是 Dalvik 的升級版。那麼 ART 又是什麼?先等等,後面會講到

3.2、Dalvik 是如何被創建且初始化的?

從上圖我們可以看到 Android 從下往上層主要分爲 4 層,如同網絡的七層協議,這樣做的好處是屏蔽本層與下層的差異。

  • linux內核層(Linux Kernel)
  • 系統運行時庫層 (Libraries and Android Runtime)
  • 應用程序框架層(Application Framework)
  • 應用程序層 (Applications)

Android 系統啓動加載完內核後,第一個執行的是 init 進程, init 進程首先要做的是設備的初始化工作,然後讀取inic.rc 文件並啓動系統中的重要外部程序 Zygote。Zygote 進程是 Android 所有進程的孵化器進程,它啓動後會首先初始化 Dalvik 虛擬機,然後啓動 system_service 進程並進入 Zygote模式,此時通過 socket 等候命令。 當執行一個 Android 應用程序時,system_service 進程通過 socket 通信方式發送命令給 Zygote,Zygote 接收到命令後會立即 fork 自身創建一個 Dalvik 虛擬機實例來執行應用程序的入口函數,這樣一個程序的啓動就完成了。 具體流程如下:
在這裏插入圖片描述
當進程 fork 成功之後,執行的工作就交給了 Dalvik 虛擬機。

到這裏我們需要接着思考一個問題,我們平時在開發應用程序的過程中視乎沒有直接接觸 dex, 而是把應用程序代碼直接打包成了apk,既然 Dalvik 虛擬機是執行的 dex 文件,那麼打包成 apk 之後的包和 dex 文件又是怎樣的關係呢?Dalvik 虛擬機又是如何從 apk 中獲取到 dex 文件的呢?

3.3、Dalvik 的可執行文件 dex 與 apk 之間的關係?

要解決這個疑惑,首先我們先來看一看,應用程序編譯打包成 apk 的過程是怎樣的,下面有一張經典的圖可以幫助我們切入這個問題:
在這裏插入圖片描述
從 apk 打包流程可以看出,整個 apk 打包過程簡單可以分爲 7 個步驟:

  • 1、打包資源文件,生成 R.java 文件;
  • 2、處理 aidl 文件,生成相應的 Java 文件;
  • 3、編譯工程源代碼,生成相應的 .class 文件;
  • 4、轉換所有的 .class 文件,生成 classes.dex 文件;
  • 5、打包生成 Apk 文件;
  • 6、對 Apk 文件進行簽名;
  • 7、對簽名後的 Apk 文件進行對齊處理;

其中每一個步驟轉換都由對應的工具完成,具體如下:

工具名稱 功能介紹 在操作系統中的路徑
aapt Android資源打包工具 ${ANDROID_SDK_HOME}/platform-tools/appt
aidl Android接口描述語言轉化爲.java文件的工具 ${ANDROID_SDK_HOME}/platform-tools/aidl
javac Java Compiler java代碼轉class文件 ${JDK_HOME}/javac或/usr/bin/javac
dex 轉化.class文件爲Davik VM能識別的.dex文件 ${ANDROID_SDK_HOME}/platform-tools/dx
apkbuilder 生成apk包 ${ANDROID_SDK_HOME}/tools/opkbuilder
jarsigner .jar文件的簽名工具 ${JDK_HOME}/jarsigner或/usr/bin/jarsigner
zipalign 字節碼對齊工具 ${ANDROID_SDK_HOME}/tools/zipalign

對 Apk 打包詳細步驟感興趣的同學可以移步apk打包詳細流程

回到本文,那麼 apk 和 dex 的關係從打包過程中就可以發現,是一個包含關係, apk 中包含着 classes.dex,AndroidManifest.xml,resources.arsc 等,這裏解壓一個實際項目,包含內容如下圖:

在這裏插入圖片描述
ok,搞明白這個關係,我們就清楚了最終打包會生成 apk 文件,此文件包含 Dalvik 虛擬機需要的可執行文件 classes.dex 。但這個過程中還有一個步驟,我們不要被忽略,就是 apk 被安裝到手機的這個過程,Android 發展至今,Google 團隊從未放棄過對這個過程進行優化,以此來提升 Android 程序的運行效率。那麼安裝過程發生了什麼,我們一起來看看:
Android 系統接收到請求需要安裝 apk 程序時,會啓動 PackageInstallerActivity ,並接收通過 Intent 傳遞過來的 apk 文件信息。

PackageInstaller 的源碼位於 Android 系統源碼的 packages\PackageInstaller 目錄。當 PackageInstallerActivity 啓動時,會首先初始化一個 PackageManager 與 Package-Parser.Package 對象,接着調用 PackageUtil 類的靜態方法 getPackageInfo() 解析程序包信息,如果這一步解析出錯,程序就會失敗返回,如果成功就會調用 setContentView() 設置 PackageInstallerActivity 的顯示視圖,接着調用 PackageUtil 類的靜態方法 getAppSnippet() 與 initSnippetForNewApp() 來設置 PackageInstallerActivity 的控件顯示程序的名稱與圖標,最後調用 initiateInstall() 方法進行一些其它的初始化工作。

顯示界面如下:
在這裏插入圖片描述
當用戶點擊了安裝按鈕時,會構建一個 Installer 對象進行程序的安裝工作,其核心代碼是通過 socket 向 /system/bin/installd 程序發送 install 指令,installd 程序是開機後常駐與內存中的, 最終調用 install()函數完成對 apk 的安裝工作。

Dalvik 是如何加載並執行 dex 文件的?

Dalvik 虛擬機首先通過 loadClassFromDex() 函數完成類的加載工作,每個類被成功解析後都擁有一個 ClassObject 類型的數據結構存儲在運行時環境中,虛擬機使用 gDvm.loadedClasses 全局哈希表來存儲與查詢所有裝載進來的類,隨後,字節碼驗證器使用 dvmVerifyCodeFlow() 函數對裝載進來的代碼進行校驗,防止 apk 被惡意篡改,接着虛擬機調用 FindClass() 函數查找並裝載 main() 方法類,最後調用 dvmInterpret() 函數初始化解釋器並執行字節碼流,一個程序的執行過程如下:

在這裏插入圖片描述

4、關於JIT 與 AOT,以及 ART 的問世

通過觀察上面虛擬機執行程序過程我們發現虛擬機只查找並裝載 main() 方法類,如果我們對編譯過程不做優化的話,每次執行代碼,都需要 Dalvik 將 dex 代碼翻譯爲微處理器指令,然後交給底層系統處理,這樣效率顯然不高,在這種背景下, JIT 即時編譯技術問世。

4.1 關於 Dalvik 虛擬機 JIT (即時編譯)

JIT(Just-in-time Compilation,即時編譯),又稱動態編譯,是一種通過在運行時動態的將字節碼翻譯爲機器碼的技術,使得程序的執行速度更快。

Android 在 2.2 版本中添加了 JIT 編譯器,當 App 運行時,每當遇到一個新類,JIT 編譯器就會對這個類進行即時編譯,經過編譯後的代碼,會被優化成相當精簡的原生型指令碼(即 native code),這樣在下次執行到相同邏輯的時候,速度就會更快。JIT 編譯器可以對執行次數頻繁的 dex/odex 代碼進行編譯與優化,將 dex/odex 中的 Dalvik Code(Smali 指令集)翻譯成相當精簡的 Native Code 去執行,JIT 的引入使得 Dalvik 的性能提升了 3~6 倍。

但 JIT 編譯有也有明顯的缺點:

  • 每次啓動應用都需要重新編譯(沒有緩存)
  • 運行時比較耗電,耗電量大

4.2 關於 ART 虛擬機 AOT(提前編譯)

回到文章開頭 -》虛擬機在Android 系統中扮演的角色時,彷彿再這裏又一次看到 ART 的身影,下面我們就來揭開它的面紗,首先先來看看什麼是 AOT:

AOT 是指 “Ahead Of Time”,與 “Just In Time” 不同,從字面來看是說提前編譯。

考慮到 JIT 的缺點,所以 Google 在 4.4 推出了全新的虛擬機運行環境 ART(Android RunTime),用來替換Dalvik(4.4上 ART 和 Dalvik 共存,用戶可以手動選擇,5.0 後 Dalvik 被替換)

AOT 是靜態編譯,應用在安裝的時候會啓動 dex2oat 過程把 dex 預編譯成 ELF 文件,每次運行程序的時候不用重新編譯。 同時 ART 對 Garbage Collection(GC)過程的也進行了改進,其優點如下:

  • 只有一次 GC 暫停(Dalvik 需要兩次)
  • 在 GC 保持暫停狀態期間並行處理
  • 在清理最近分配的短時對象這種特殊情況中,回收器的總 GC 時間更短
  • 優化了垃圾回收的工效,能夠更加及時地進行並行垃圾回收,這使得 GC_FOR_ALLOC 事件在典型用例中極爲罕見
  • 壓縮 GC 以減少後臺內存使用和碎片

但是,但是,但是 AOT 也不是現在的終極目標,它也有缺點:

  • 應用安裝和系統升級時時間較長(需要預編譯)
  • 優化後的文件會佔用額外的存儲空間(測試發現會增加 10 - 20% 左右)

JIT 和 AOT 共存

Android 7.0上,JIT 編譯器被再次使用,採用 AOT/JIT 混合編譯的策略,特點是:

  • 應用在安裝的時候 dex 不會再被編譯
  • App 運行時, dex 文件先通過解析器被直接執行,熱點函數會被識別並被 JIT 編譯後存儲在 jit code cache 中並生成 profile 文件以記錄熱點函數的信息。
  • 手機進入 IDLE(空閒) 或者 Charging(充電) 狀態的時候,系統會掃描 App 目錄下的 profile 文件並執行 AOT 過程進行編譯。

優點也就是結合了 JIT 與 AOT 的優點:

  • 即使是大應用,安裝時間也能縮短到幾秒
  • 系統升級能更快地安裝,因爲不再需要優化這一步
  • 應用的內存佔用更小,有些情況下可以降低50%
  • 改善了性能
  • 更低的電池消耗
  • 更高效的 GC 回收

拓展

雖然目前 Android 程序的運行流暢度越來越可觀,但明顯還沒有達到用戶滿意的程度,作爲開發者,我們的任務任重而道遠,瞭解本節內容,掌握 Android 程序運行背後的內容,有助於我們開發出更高效應用以及突破新技術的瓶頸,例如插件化與熱更新的原理髮生在虛擬機通過 Classloder 裝載 dex 的過程中,利用 JIT 及時編譯寫出高質量的重複使用代碼等等。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章