上圖是Android整體的架構,Android Runtime之於Android而言相當於心臟之於人體,是Android程序加載和運行的環境。這篇文章主要針對Android Runtime部分進行展開,探討Android Runtime的發展以及目前現狀,並介紹應用Profile-Guided Optimization(PGO)技術對應用啟動速度進行優化的可行性。
App運行時演進JVM
Android原生代碼使用Java或者Kotlin編寫,這些代碼會通過javac或者kotlinc編譯成.class文件,在Android之前,這些.class文件會被輸入到JVM中執行。JVM可以簡單分為三個子系統,分別是Class Loader、Runtime Data Area以及Execution Engine。其中Class Loader主要負責加載類、校驗字節碼、符號引用鏈接及對靜態變量和靜態方法分配內存並初始化。Runtime Data負責存儲數據,分為方法區、堆區、棧區、程序計數器以及本地方法棧。Execution Engine負責二進制代碼的執行以及垃圾回收。
Execution Engine中,會採用Interpreter或者JIT執行。其中Interpreter表示在運行的過程中對二進制代碼進行解釋,每次執行相同的二進制代碼都進行解釋比較浪費資源,因此對於熱區的二進制代碼會進行JIT即時編譯,對二進制代碼編譯成機器碼,這樣相同的二進制代碼執行時,就不用再次進行解釋。
DVM(Android 2.1/2.2)
JVM是stack-based的運行環境,在移動設備中對性能和存儲空間要求較高,因此Android使用了register-based的Dalvik VM。從JVM轉換到DVM我們需要將.class文件轉換為.dex文件,從.class轉換到.dex的過程需要經過 desugar -> proguard -> dex compiler三個過程,這三個過程後來逐步變成 proguard -> D8(Desugar) 直到演變到今天只需要一步R8(D8(Desugar))。
我們主要關注Android中Runtime Engine與JVM的區別。在Android早期的版本裡面,只存在Interpreter解釋器,到了Android2.2版本將JIT引入,這個版本Dalvik與JVM的Runtime Engine區別不大。
ART-AOT(Android 4.4/5.0)
為了加快應用的啟動速度和體驗,到了Android4.4,Google提供了一個新的運行時環境ART(Android Runtime),到了Android5.0,ART替換Dalvik成為唯一的運行時環境。
ART運行時環境中,採用了AOT(Ahead-of-time)編譯方式,即在應用安裝的時候就將.dex提前編譯成機器碼,經過AOT編譯之後.dex文件會生成.oat文件。這樣在應用啟動執行的時候,因為不需要進行解釋編譯,大大加快了啟動速度。
然而AOT帶來了以下兩個問題:
應用安裝時間大幅增加,由於在安裝的過程中同時需要編譯成機器碼,應用安裝時間會比較長,特別在系統升級的時候,需要對所有應用進行重新編譯,出現了經典的升級等待噩夢。
應用占用過多的存儲空間,由於所有應用都被編譯成.oat機器碼,應用所占的存儲空間大大增加,使得本來並不充裕的存儲空間變得雪上加霜。
進一步思考對應用全量進行編譯可能是沒有必要的,因為用戶可能只會用到一個應用的部分常用功能,並且全量編譯之後更大的機器碼加載會占用IO資源。
ART-PGO(Android 7.0)
從Android7.0開始,Google重新引入了JIT的編譯方式,不再對應用進行全量編譯,結合AOT、JIT、Interpreter三者的優勢提出了PGO(Profile-guided optimization)的編譯方式。
在應用執行的過程中,先使用Interpreter直接解釋,當某些二進制代碼被調用次數較多時,會生成一個Profile文件記錄這些方法存儲起來,當二進制代碼被頻繁調用時,則直接進行JIT即時編譯並緩存起來。
當應用處於空閒(屏幕關閉且充電)的狀態時,編譯守護進程會根據Profile文件進行AOT編譯。
當應用重新打開時,進行過JIT和AOT編譯的代碼可以直接執行。
這樣就可以在應用安裝速度以及應用打開速度之間取得平衡。
JIT 工作流程:
ART-Cloud Profile(Android 9.0)
不過這裡還是有一個問題,就是當用戶第一次安裝應用的時候並沒有進行任何的AOT優化,通常會經過用戶多次的使用才能使得啟動速度得到優化。
考慮到一個應用通常會有一些用戶經常使用執行的代碼(例如啟動部分以及用戶常用功能)並且大多數時候會有先行版本用於收集Profile數據,因此Google考慮將用戶生成的Profile文件上傳到Google Play中,並在應用安裝時同時帶上這個Profile文件,在安裝的過程中,會根據這個Profile對應用進行部分的AOT編譯。這樣當用戶安裝完第一次打開的時候,就能達到較快的啟動速度。
Profile in cloude 需要系統應用市場支持,在國內市場使用Google Play的占比非常低,因此cloud profile的優化在國內幾乎是沒有作用的,不過Profile的機制提供了一個可以做啟動優化的思路。早在2019年,支付寶就在秒開技術的回應的裡面提到過profile-based compile的技術,參考:如何看待今日頭條自媒體發布謠言稱「支付寶幾乎秒開是因為採用華為方舟編譯器」?,這也是我們一直研究Profile技術的原因。困擾着我們的一直有兩個問題,第一個問題是如何生成Profile文件,第二個問題是怎麼使用生成的Profile文件。對於第一個問題的解決相對還是有思路的,因為app運行就會生成profile文件,因此我們手動運行幾次app就能在文件系統中收集到這個文件,不過如何以一種較為自動化的手段收集仍然是個問題。第二個問題我們知道Profile文件最終生成的位置,因此我們可以把生成的文件放到相應的系統目錄,不過大多數手機和應用都沒有權限直接放置這個文件。因此Profile優化技術一直都沒有落地,直到Baseline Proflie讓我們看到了希望。
Baseline Profile
Baseline Profile是一套生成和使用Profile文件的工具,在2022年一月份開始進入視野,隨後在Google I/O 2022隨着Jetpack新變化得到廣泛關注。其背景是Google Map加快了發版速度,Cloud Profle還沒完全收集好就上新版,導致Cloud Proflie失效。還有一個背景是Jetpack Compose 不是系統代碼,因此沒有完全編譯成機器碼,而且Jetpack Compose庫比較大,因此在Profile生成之前使用了Jetpack Compose的應用啟動會產生性能問題。最後Google為了解決這些問題,創造了收集Profile的BaselineProfileRule Macrobenchmark以及使用Profile的ProfileInstaller。
使用Baseline Profile的機制可以在Android7及以上的手機上得到應用的啟動加速,因為從上述知道Android7就已經開始有PGO(Profile-guided optimization)的編譯方式。生成的Profile文件會打包到apk裡面,並且會結合Google Play的Cloud Profile來引導AOT編譯。雖然在國內基本上用不了Cloud Profile,不過Baseline Profile是可以獨立於Google Play單獨使用的。
在使用了Baseline Proflie之後,有道詞典的啟動速度從線上統計上看,冷啟動時間有15%的提升。
這篇文章主要介紹了Android Runtime的演進以及對於應用啟動的影響,下一篇文章我會詳細介紹關於Profile&dex文件優化、Baseline Profile工具庫原理,以及在實際操作上如何使用的問題,敬請大家期待一下!