android 中的虛拟機(1) 漫談編譯
在說虛拟機之前,先提一些題外話。
我們說的程式設計,其實可以類比寫文章。道理是一樣的。
隻不過我們寫的文章是給機器看的。機器不認識人類語言。是以就有了計算機語言。友善我們跟計算機溝通。
但是呢,計算機隻認識二進制,0110101010這樣的序列。
然後計算機能工作的原因是因為計算機晶片中預置了相關的指令集。
說白了就像是設計好了一套規則,這規則通過特殊手段刻錄到晶片中。比如說 當 CPU 收到 01110 就說明要從記憶體中讀取某個資料。
是以剛開始是二進制程式設計。通過給機器發送特定的二進制序列來完成工作。那這時候程式員程式設計的代碼就類似這樣
0000 0010 1101 0100
1101 0100 0111 0000
很明顯這種不是人看的代碼不僅晦澀難懂,還難以維護,開發效率也十分低下。于是就出現了相對比二進制程式設計要更進階的語言。就是現在說的彙編語言。
彙編類似于
ADD R2 ,5
這是一條arm 彙編指令。相對于之前的二進制數。可讀性高了不少。起碼不容易眼瞎了。要想讓計算機讀懂這個彙編代碼,這中間少不了一個解釋器,一個能将彙編語言轉成計算機能讀懂的二進制代碼的工具。這個工具我們可以簡單叫它為語言解釋器。當然這種解釋器需要精通語言翻譯的規則,這樣才能準确無誤翻譯我們的彙程式設計式。
但是問題出來了,彙程式設計式雖然可讀性較高,但是随着大型程式和更加複雜的應用場景出現,使用彙編語言進行程式開發依舊出現了效率低下,程式維護成本高等一系列問題。于是更進階的語言出現了。比如,c,c++、 java這些語言。也就是我們現在說的進階語言。
還是重複昨天的故事,就像從二進制程式設計發展成為彙編程式設計一樣,進階語言的開發更加符合曆史潮流。科學家們用的一樣的套路,設計出更加人性化,可讀性更高,了解使用友善的文法規則。比如:
int a = 3+4
這是進階語言中簡單的指派語句。是不是簡單到猜都能猜出來。
那a = 3+4,即a=7呗。
确實是這樣。
使用更加人性化的文法規則,按照文法規則設計出能将進階語言轉化為更貼近機器語言的語言解釋器。其實就是程式設計語言最根本的目的。當然轉化過程并不是一步到位的,可以先将進階語言轉為符合某種規範的彙編語言,這樣就能複用前人的彙編語言解釋器,将轉化得到的彙編代碼再次轉為二進制。那這一系列轉化的過程,就叫編譯。
感覺好像有點出來了是不是。
我們所謂的語言,隻不過是一套規則。不同的語言就是不同的規則。規則中有自己的關鍵字,自己的文法結構, 自己的資料類型等等。無論是什麼計算機語言,大緻如此。
那麼我們前面提到的語言解釋器,其實就可以看成是語言的一種實作。比如,我們的C++, 可以看成是一套程式設計規範。按照規範的釋出修訂時間有c++98,c++03,c++11等等。
比如說:
c++98 就是1998年釋出的國際标準
c++03 就是2003年釋出的國際标準
c++11 就是2011年釋出的...
不同的規則,自然需要不同的解釋器來解釋,或者叫編譯器。然後各大廠商就根據這些标準設計出了自己的解釋器。比如,有需要在windows下運作c++11的需要。于是就有了c++11的windows 編譯器。就像Cygwin, MinGW, Microsoft Visual c++。linux 下也有 GUN C++ 編譯器。
是以我們可以了解成,編譯器就是語言的特定實作。
android 中的虛拟機(2) Dalvik虛拟機
回到android。很多人知道android 的應用程式是一個個APK包。是使用JAVA編寫的,雖然現在還有kotlin這些語言,甚至還有跨平台方案,比如說Flutter。但我們依舊繞不開APK,依舊繞不開對應的解釋器。比如其中使用到了Dalvik。
現在想想,Dalvik何嘗不能看成一種JVM(java Virtual Machine )的實作?跟我們在PC上跑java是一樣的。
隻不過在Android這種可移動裝置中,資源是很緊張的,具體有好幾個方面,比如:
1、每個java類都會生成class檔案。使得加載的時候需要頻繁讀取檔案,加載速度慢
2、檔案的加載,堆棧的加棧模式都需要耗費較多的記憶體資源,對于記憶體資源稀缺的移動端,是一個明顯的短闆。
簡言之,移動端的資源更加有限,無論是記憶體,cpu,磁盤都不能跟伺服器端比較,是以class檔案在移動端運作是不太合适的。于是乎dex檔案就誕生了。我們大概可以了解成将多個class檔案整合成打包成一個dex檔案。而且做了較多的優化,比如:
2.1、将多個class整合成一個dex,減少反複讀取檔案的時間,提高加載速度。
2.2、dex中對各個 各個資料緊密排列,無間隙,結構的設計上大量使用了索引,減少了檔案體積。
那麼dex檔案被生成出來了,自然就不能用原來的JVM來解析和運作。是以在android上有自己的虛拟機,也就是我們常說的Dalvik虛拟機。
在android 4.4及以前使用的都是Dalvik虛拟機,運作的就是我們的dex檔案。但是這個dex 檔案并不是我們機器可以直接運作的機器碼,是以在dalvik運作dex的時候,會先将dex快速轉為可以運作的機器碼。
而且我們前面也提到過了,dex檔案是有大小限制的,而且很多Android開發者可能知道一個65535問題,說的就是單個dex的方法數不得超過65535的上限。
于是乎比較大的apk中往往都會包含多個dex,這樣dalvik虛拟機在應用冷啟動的過程中就會加上一個合包的過程,這樣導緻的結果就是我們app的啟動和運作都會比較慢。
為了解決這個問題,在android 5.0隻有就開始有了ART虛拟機。
android 中的虛拟機(3) ART虛拟機
但是為了相容android4.4及以下的程式,這個ART虛拟機也需要有Dalvik虛拟機的特性,但是就在這個基礎上加上了一個很好的特性AOT(ahead oftime),根據英文可以簡單了解成,運作前編譯。
但是 從Android 7.0(代号 Nougat,簡稱 N)開始,又引入了JIT編譯,以及AOT 與JIT 混合使用的ART虛拟機
這裡簡單介紹下這個ART 虛拟機。
ART虛拟機從7.0開始就混合使用3種編譯方式
1、使用預先 (AOT) 編譯:程式運作前編譯好
2、即時 (JIT) 編譯:一邊運作一邊編譯
3、配置檔案引導型編譯
這些編譯方式互相組合,共同為程式運作提供最優的服務。
例如,Pixel 裝置配置了以下編譯流程:
- 最初安裝應用時不進行任何 AOT 編譯。應用前幾次運作時,系統會對其進行解譯,并對經常執行的方法進行JIT 編譯。
- 當裝置閑置和充電時,編譯守護程式會運作,以便根據在應用前幾次運作期間生成的配置檔案對常用代碼進行 AOT 編譯。
- 下一次重新啟動應用時将會使用配置檔案引導型代碼,并避免在運作時對已經過編譯的方法進行 JIT 編譯。在應用後續運作期間經過 JIT 編譯的方法将會添加到配置檔案中,然後編譯守護程式将會對這些方法進行 AOT 編譯。
ART 包括一個編譯器(dex2oat 工具)和一個為啟動 Zygote 而加載的運作時 (libart.so)。dex2oat 工具接受一個 APK 檔案,并生成一個或多個編譯工件檔案,然後運作時将會加載這些檔案。檔案的個數、擴充名和名稱因版本而異。
也就是說,dex在程式運作前就經過dex2oat 編譯,編譯後會生成.art、.oat兩個檔案,oat是一個android定制的elf檔案,原始dex也儲存在其中。
在8.0後,dex單獨儲存到.vdex檔案中,另外還有經過AOT編譯的odex 檔案和art檔案
.vdex:其中包含 APK 的未壓縮DEX 代碼,以及一些旨在加快驗證速度的中繼資料。
.odex:其中包含 APK 中已經過AOT 編譯的方法代碼。
.art(optional):其中包含 APK 中列出的某些字元串和類的 ART 内部表示,類似于一個記憶體映像,緩存常用的ArtField、ArtMethod、DexCache等内容,加載後可直接使用,避免解析耗時。
可以想象,ART虛拟機将.dex檔案提前做了一個優化處理,是以不會有一個合包的過程,這樣ART虛拟機的使用會很大的提升APP冷啟動速度。
下面來說說JIT編譯
Android Runtime (ART) 包含一個具備代碼分析功能的即時 (JIT) 編譯器,該編譯器可以在 Android 應用運作時持續提高其性能。JIT 編譯器對 Android 運作元件目前的預先 (AOT) 編譯器進行了補充,可以提升運作時性能,節省存儲空間,加快應用和系統更新速度。相較于AOT 編譯器,JIT編譯器的優勢也更為明顯,因為在應用自動更新期間或在無線下載下傳(OTA) 更新期間重新編譯應用時,它不會拖慢系統速度。
官方給出了這樣一個圖,其實很清楚了
1、如果應用在運作前已經經過AOT編譯了,那就會有oat 檔案。這個oat 直接通過ART就可以運作了
2、如果沒有經過AOT編譯,那就還是dex 檔案。這時候在運作時對dex指令進行interpreter,生成機器碼運作。
3、ART根據函數調用的次數來決定熱點代碼,并以函數為次元将熱點代碼的機器碼進行緩存,以便下一次更加快捷的調用該機器碼。
Android 的編譯器不斷更新,這是android 平台本身不斷發展的結果。每一個新的解釋器推出,總是要做出一些優化,或者提高效率,或者降低資源損耗。最終目标肯定是以人為本,讓大衆享受到更好的android 服務。
vx公衆号: 程式員hunter