原文地址:juejin.cn/post/684490…android
轉載請署名,嚴禁抄襲程序員
本文已受權微信公衆號:鴻洋(hongyangAndroid)原創首發編程
最近華爲方舟編譯器要開源了,筆者去看了下發佈會PPT,發現做爲一名Android開發者,PPT中所介紹的知識點我竟然不能徹底看懂???因而乎惡補了下PPT中的內容,整理成本文。bash
本文將用通俗的語言從底層介紹Android卡頓的歷史緣由和谷歌與之鬥爭的過程微信
閱讀完這篇文章後你將markdown
理解計算機是如何解讀咱們所寫的程序並執行相應功能的架構
瞭解Android虛擬機的進化史jvm
從底層瞭解形成Android卡頓的三大緣由編程語言
首先咱們須要補習下一些基礎概念,來理解計算機是如何解讀咱們所寫的程序並執行相應功能的。oop
某些編程語言(如Java)的源代碼經過編譯-解釋的流程可被計算機讀懂
先上一段Java代碼
public static void main(String[] args){ print('Hello World') } 複製代碼
這是全部程序員的第一課,只須要寫完這段代碼並執行,電腦或手機就會打印出Hello World
。 那麼問題來了,英文是人類世界的語言,計算機(CPU)是怎麼理解英文的呢?
衆所周知,0和1是計算機世界的語言,能夠說計算機只認識0和1。 那麼咱們只須要把上面那段英文代碼只經過0和1表達給計算機,就可讓計算機讀懂並執行。
結合上圖,Java源代碼經過編譯
變成字節碼,而後字節碼按照模版中的規則解釋
爲機器碼。
機器碼
機器碼就是能被CPU直接解讀並執行的語言。
可是若是使用上圖中生成的機器碼跑在另一臺計算機中,極可能就會運行失敗。
這是由於不一樣的計算機,可以解讀的機器碼可能不一樣。通俗而言就是能在A電腦上運行的機器碼,放到B電腦上就可能就很差使了。
舉個🌰,中國人A認識中文,英語;俄國人B認識俄語,英語。這時他兩同時作一張中文試卷,B大概連寫名字的地方都找不到。
因此這時候咱們須要字節碼。
字節碼
中國人A看不懂俄文試卷,俄國人B看不懂中文試卷,可是你們都看得懂英文試卷。
字節碼就是個中間碼
,Java能編譯爲字節碼,同一份字節碼能按照指定模版的規則解釋爲指定的機器碼
。
字節碼的好處:
1.實現了跨平臺,一份源代碼只須要編譯成一份字節碼,而後根據不一樣的模版將字節碼解釋成當前計算機認識的機器碼,這就是Java所說的「編譯一次,處處運行」。
2.同一份源碼
被編譯成的字節碼
大小遠遠小於機器碼
。
編譯語言
咱們熟知的C/C++語言,是
編譯語言
,即程序員編譯以後能夠一步到位(編譯成機器碼),能夠被CPU直接解讀並執行。
可能有人會問,既然上文中說過字節碼
有種種好處,爲何不使用字節碼
呢?
這是由於每種編程語言設計的初衷不一樣,有些是爲了跨平臺而設計的,如Java,但有些是針對某個指定機器或某批指定型號的機器設計的。
舉個🌰,蘋果公司開發的OC語言和Swift語言,就是針對自家產品設計的,我才無論你其餘人的產品呢。因此OC或Swift語言設計初衷之一就是快,可直接編譯爲機器碼使iPhone或iPad解讀並執行。這也是爲何蘋果手機的應用比安卓手機應用大的主要緣由。這更是爲何蘋果手機更流暢的緣由之一!(沒有中間商賺差價)
編譯-解釋語言
拿開發Android的語言Java爲例,Java是編譯-解釋語言,即程序員編譯以後不能夠直接編譯爲機器碼,而是會編譯成字節碼(在Java程序中爲.class文件,在Android程序中爲.dex文件)。而後咱們須要將字節碼再解釋成機器碼,使之能被CPU解讀。
這第二次解釋,即從字節碼解釋成機器碼的過程,是程序安裝或運行後,在Java虛擬機中實現的。
今年最新的Android版本已是10了,其實在這兩年關於Android手機卡頓的聲音已經慢慢低了下去,取而代之的是流暢如iOS之類的聲音。
可是諸如超過iOS的話,還比較少,實際上是由於Android有卡頓有三大歷史緣由。起步就比iOS低。
經過上文描述,咱們能夠知道,iOS之因此不卡是由於他一步到位,省略了中間解釋的步驟,直接跟硬件層進行通訊。而Android因爲沒有一步到位,每次執行都須要實時解釋成機器碼,因此性能較iOS明顯低下。
咱們已經明確知道了字節碼(中間商)是形成卡頓的主要元兇之一,咱們能否像iOS那樣扔掉字節碼,直接一步到位呢?
明顯不能,由於iOS搞來搞去就那麼幾個機型。反觀Android方面,光手機就有無數種機型,無數種CPU架構/型號,更別提什麼平板,車載等其餘設備了。有那麼多類型的硬件設備表明着就有很是多不一樣的硬件架構,每種架構都有本身對應的機器碼解釋規則。顯然像iOS那樣一步到位是不現實的。
那怎麼辦呢?既然扔不掉字節碼這個中間商,那咱們只能剝削他咯,讓整個解釋的過程快一點,再快一點。而解釋所在的「工廠」在虛擬機內。
接下來就是偉大的Android虛擬機進化之路!
DVM是Google開發的Android平臺虛擬機,可讀取.dex的字節碼。 上文中所說的從字節碼解釋成機器碼
的過程在Java虛擬機中,在Android平臺中虛擬機指的就是這個DVM。 在Android1.0時期,程序一邊運行,DVM中的解釋器(翻譯機)一邊解釋字節碼
。 可想而知,這樣效率絕對低下。一個字,卡。
其實解決DVM的問題思路很清楚,咱們在程序某個功能運行前就解釋
就能夠了。
在Android2.2時期,聰明的谷歌引入了JIT(Just In Time)機制,直譯就是即時編譯。
舉個🌰,我常常去一家餐館吃飯,老闆已經知道我想吃什麼菜了,在我到以前就把菜準備好了,這樣我就省去了等菜的時間。
JIT就至關於這個聰明的老闆,它會在手機打開APP時,將用戶常用的功能記下來。當用戶打開APP的時候立馬將這些內容編譯出來,這樣當用戶打開這些內容時,JIT已經將'菜'準備好了。這樣就提升了總體效率。
雖然JIT挺聰明的,且整體思路清晰理想豐滿,但現實是仍然卡的要死。
存在的問題:
聰明的谷歌又想到個方法,既然咱們能在打開APP的時候將字節碼
編譯成機器碼
,那麼咱們何不在APP安裝的時候就把字節碼
編譯成機器碼
呢?這樣每次打開APP也不用重複勞動了,一勞永逸。
這確實是個思路,因而谷歌推出了ART來替代DVM,ART全稱Android Runtime,它在DVM的基礎上作了一些優化,它在應用被安裝的時候就將應用編譯成機器碼
,這個過程稱爲AOT(Ahead-Of-Time),即預編譯。
可是問題又來了,打開APP是不卡了,可是安裝APP慢的要死,可能有人會說,一個APP又不是會頻繁安裝,能夠犧牲下這點時間。 可是很差意思,安卓手機每次OTA啓動(即系統版本更新或刷機後)都會從新安裝全部APP,無奈吧!絕望吧!對,還記得那兩年,被安卓版本更新所支配的恐懼嗎!
谷歌最終祭出了終極大招,DVM+JIT很差,ART+AOT又很差。行,我把他們都混合起來,那總能夠了吧!
因而谷歌在Android7.0的時候,發佈了混合編譯。 即安裝時先不編譯成機器碼
,在手機不被使用的時候,AOT偷偷的把能編譯成機器碼
的那部分代碼編譯了(至於什麼是能編譯的部分,下文字節碼的編譯模板
詳述)。其實就是把以前APP安裝時候乾的活偷偷的在手機空的時候幹了。
若是來不及編譯的話,再把JIT和解釋器這對難兄難弟叫起來,讓他們去編譯或實時解釋。
不得不佩服谷歌這粗暴的解決問題的方式,這樣一來確實Android手機從萬年卡頓慢慢的坑中出來了。
在Android8.0時期,谷歌又盯上了解釋器,其實縱觀上面的問題,根源就是這個解釋器解釋的太慢了!(什麼JIT,AOT,老夫解釋只有一個字,快)那咱們何不讓這個解釋器解釋的快一點呢? 因而谷歌改進了解釋器,解釋模式執行效率大大提高。
這個點會在下文字節碼的編譯模板
中詳述。
這邊簡單而言就是,在Android9.0上提供了預先放置熱點代碼的方式,應用在安裝的時候就能知道經常使用代碼會被提早編譯。(借用知乎@weishu大神的原話)
JNI又稱爲 Java Native Interface,翻譯過來就是Java原生接口,就是用來跟C/C++代碼交互的。
若是不作Android開發的可能不知道,Android項目裏的代碼除了Java,頗有可能還有部分C語言的代碼。
這個時候有個嚴重的問題,首先上圖 (圖片參考方舟編譯器原理PPT):
在開發階段Java源代碼在開發階段打包成.dex文件,C語言直接就是.so庫,由於C語言自己就是編譯語言。
在用戶手機中,APK中的.dex文件(字節碼)會被解釋爲.oat文件(機器碼)運行在ART虛擬機中,.so庫則爲計算機能夠直接運行的二進制代碼(機器碼),兩份機器碼要互相調用確定是有開銷的。
下面就來闡述下爲何兩份機器碼會不一樣。
這邊須要深刻理解字節碼->機器碼
的編譯過程,在圖上雖然都被編譯成了機器碼,都能被硬件直接調用,可是兩份機器碼的性能,效率,實現方式相差甚多,這主要是由如下兩個點形成的:
編程語言不一樣致使編譯出的字節碼
不一樣致使編譯出的機器碼
不一樣。
舉個🌰,針對一樣是靜態語言的C和Java,對int a + b 的運算
C語言能夠直接加載內存,在寄存器中計算,這是因爲C語言是靜態語言,a和b是肯定的int對象。
在Java中雖然定義對象咱們也要明確的指出對象的類型,例如int a = 0,可是Java擁有動態性,Java擁有反射,代理,誰也不敢保證a在被調用時仍是int類型,因此Java的編譯須要考慮上下文關係,即具體狀況具體編譯。
因此連字節碼
已經不一樣了,編譯出的機器碼
確定不一樣。
運行環境
不一樣致使編譯出的機器碼
不一樣
圖中明顯看到由Java編譯而來的機器碼
包裹在ART中,ART全稱Android RunTime,即安卓運行環境,跟虛擬機差很少是一個意思。而C語言所在的運行環境不在ART中。
RunTime提供了基本的輸入輸出或是內存管理等支持,若是要在兩個不一樣的RunTime中互相調用,則必然有額外開銷。
舉個🌰,因爲Java有GC(垃圾回收機制),在Java中的一個對象地址不是固定的,有可能被GC挪動了。即在ART環境中跑的機器碼中的對象的地址不固定。但是C語言哪管那麼多幺蛾子,C就直接問Java要一個對象的地址,但萬一這個對象地址被挪動了,那就完蛋了。解決方案有兩個:
(此處參考知乎@張鐸在華爲公佈的方舟編譯器到底對安卓軟件生態會有多大影響?中的回答)
咱們舉個🌰來理解編譯模版,「Hello world」能夠被翻譯爲「你好,世界」,一樣也能夠被翻譯爲「世界,你好」,這個差異就是
編譯模版
不一樣致使的,
字節碼
能夠經過不一樣的編譯模版被編譯爲機器碼
,而編譯模版的不一樣將直接致使編譯完後的機器碼
性能截然不同。
在安卓中,ART有一套規定的,統一的編譯模版,暫且稱爲VM模版
,這套模版雖算不上差勁,但也算不上優秀。
由於它是谷歌爸爸搞出來的,確定算不上差勁,但因爲沒有針對每個APP進行特定的優化,因此也算不上優秀。
問題就存在於沒有針對每個APP進行優化。
在上文谷歌對於Android2.2的虛擬機優化
中已經講到過,那時候谷歌使用JIT將用戶經常使用的功能記下來(熱點代碼),當用戶打開APP的時候立馬將這些內容編譯出來,即優先編譯熱點代碼
。
可是到了Android7.0的混合編譯時代,因爲AOT的存在,這個功能被弱化了,這時JIT記錄下的熱點代碼並不是是持久化的。AOT的編譯優先級遵循於vm模版,AOT根據模板的內容將一些字節碼
優先編譯爲機器碼
。
那麼這個時候就產生了一個問題。
先舉個🌰,一家中餐館的招牌菜是番茄炒蛋,那麼番茄炒蛋的備菜確定很足,可是顧客A特立獨行,他恰恰不要吃番茄炒蛋,他每次都點一個冷門的牛排套餐,那這時候只能讓顧客等着老闆將牛排套餐作完。
若是一個APP的熱點代碼(如首頁),恰好遊離於VM模板以外,那麼AOT就其實形同虛設了。(好比vm模版優先編譯名稱不大於15個字符的類和方法,可是首頁的類名恰好高於15個字符。此處僅爲舉例並無實際論證過)
下面用首頁和設置頁來舉例:因爲遵循vm模版,AOT由於某個緣由沒有優先編譯首頁部分代碼,而轉而去編譯了不過重要的設置頁代碼:
上圖的流程說明了在特殊狀況下,AOT編譯實則不起做用,徹底是靠解釋器和JIT在進行實時編譯,整個編譯方案退步到了Android2.2時期。
雖然這個問題存在,但並非特別嚴重。由於ART並無我說的那麼笨。在以後應用使用過程當中,ART會記錄並學習用戶的使用習慣(保存熱點代碼
),而後更新針對當前APP的定製化vm模版,不斷的補充熱點代碼
,補充定製化模版
。
這是否是聽起來很熟悉?在手機發布大會上的宣傳語「基於用戶操做習慣進行學習,APP打開速度不斷提升」的部分原理就是這個。
其實要一勞永逸的解決這個問題思路也不難:咱們只須要在吃飯前跟老闆提早預約想吃啥就行,讓老闆先準備起來,這樣等咱們到了就不用等餐了。
在最新的Android9.0版本中,谷歌推出了這個相似提早預約的功能:編譯系統支持在具備藍圖編譯規則的原生 Android 模塊上使用 Clang 的配置文件引導優化 (PGO)。
說人話:谷歌容許你在開發階段添加一個配置文件,這個配置文件內可指定「熱點代碼」,當應用安裝完後,ART在後臺悄悄編譯APP時,會優先編譯配置文件中指定的「熱點代碼」。
雖然谷歌支持,可是這塊技術對於APP開發人員而言國內資料過於缺少,普及面不廣。筆者先貼上官方連接,以及這篇博客,其中介紹的仍是挺詳細的。(隔壁Xcode針對PGO都有UI界面了)
解決思路總結爲四個字就是:華爲方舟。
方舟的解決思路:
針對虛擬機問題,方舟說:我不要你這個爛虛擬機了,咱們裸奔
針對JNI調用問題,方舟說:咱們讓Java在編譯階段跟C同樣直接編譯成機器碼,幹掉虛擬機,跟.so庫直接調用,毫無JNI開銷問題
針對編譯模版問題,方舟說:咱們支持針對不一樣APP進行不一樣的編譯優化
總結一下:方舟支持在打包編譯階段針對不一樣APP進行不一樣的編譯優化,而後直接打包成機器碼.apk(極可能已經不叫apk了),而後直接運行。
這樣看起來方舟確實解決掉了三大問題,可是,代價呢?
若是按照這個思路,方舟就確定不止是一個編譯器了,它應該還有一套本身的runtime。固然這些都是後話了。
關於方舟的實現只是大概講了思路,但沒有深刻,由於一來方舟沒開源,二來方舟發佈會PPT營銷層面更多,技術細節缺乏,如今奇思妙想徹底是紙上談兵,一切仍是靜待開源吧。
自從發表文章以來,收到了一些反饋,其中有一種聲音是:
形成卡頓的主要緣由是垃圾代碼和保活,全家桶等國產軟件的鍋。
對這一點,我不能否認,垃圾代碼,保活策略,全家桶是很噁心。
可是若是要將這些影響上升爲形成卡頓的主要緣由,
筆者認爲大家是太看得起本身的垃圾代碼負優化能力了,仍是太看不起小米,華爲這些系統生產廠家了,仍是以爲天底下的iOS人手水平高Android一個層次呢?
若是必定要說垃圾代碼形成了卡頓,也請去理解下哪些代碼是所謂的垃圾代碼,好比某些代碼形成了內存抖動和GC頻繁回收形成了卡頓,不要就扔下一句,垃圾代碼而後讓程序員背了全部的鍋。都9102年了,別再隨便甩鍋給程序員了!,也請那些這樣認爲的人別再妄自菲薄了!
至於保活,在如今的華爲小米等系統里弄一個全天候保活,互相拉起的進程,大概就會像黑進阿里的黑客同樣,次日去公司報道吧。
至於一些千元機的卡頓問題,能夠了解下Google新推的Android Go系統,這個系統下的APP開發要求異常的苛刻。