9102年了,還不知道Android爲何卡?

原文地址:juejin.cn/post/684490…android

轉載請署名,嚴禁抄襲程序員

本文已受權微信公衆號:鴻洋(hongyangAndroid)原創首發編程

導讀

最近華爲方舟編譯器要開源了,筆者去看了下發佈會PPT,發現做爲一名Android開發者,PPT中所介紹的知識點我竟然不能徹底看懂???因而乎惡補了下PPT中的內容,整理成本文。bash

本文將用通俗的語言從底層介紹Android卡頓的歷史緣由和谷歌與之鬥爭的過程微信

閱讀完這篇文章後你將markdown

  1. 理解計算機是如何解讀咱們所寫的程序並執行相應功能的架構

  2. 瞭解Android虛擬機的進化史jvm

  3. 從底層瞭解形成Android卡頓的三大緣由編程語言

1、基礎概念

首先咱們須要補習下一些基礎概念,來理解計算機是如何解讀咱們所寫的程序並執行相應功能的。oop

1.編譯&解釋

某些編程語言(如Java)的源代碼經過編譯-解釋的流程可被計算機讀懂

先上一段Java代碼

public static void main(String[] args){
    print('Hello World')
}
複製代碼

這是全部程序員的第一課,只須要寫完這段代碼並執行,電腦或手機就會打印出Hello World。 那麼問題來了,英文是人類世界的語言,計算機(CPU)是怎麼理解英文的呢?

衆所周知,0和1是計算機世界的語言,能夠說計算機只認識0和1。 那麼咱們只須要把上面那段英文代碼只經過0和1表達給計算機,就可讓計算機讀懂並執行。

結合上圖,Java源代碼經過編譯變成字節碼,而後字節碼按照模版中的規則解釋爲機器碼。

2.機器碼&字節碼

  • 機器碼

    機器碼就是能被CPU直接解讀並執行的語言。

    可是若是使用上圖中生成的機器碼跑在另一臺計算機中,極可能就會運行失敗。

    這是由於不一樣的計算機,可以解讀的機器碼可能不一樣。通俗而言就是能在A電腦上運行的機器碼,放到B電腦上就可能就很差使了。

    舉個🌰,中國人A認識中文,英語;俄國人B認識俄語,英語。這時他兩同時作一張中文試卷,B大概連寫名字的地方都找不到。

    因此這時候咱們須要字節碼。

  • 字節碼

    中國人A看不懂俄文試卷,俄國人B看不懂中文試卷,可是你們都看得懂英文試卷。

    字節碼就是個中間碼,Java能編譯爲字節碼,同一份字節碼能按照指定模版的規則解釋爲指定的機器碼

    字節碼的好處:

    1.實現了跨平臺,一份源代碼只須要編譯成一份字節碼,而後根據不一樣的模版將字節碼解釋成當前計算機認識的機器碼,這就是Java所說的「編譯一次,處處運行」。

    2.同一份源碼被編譯成的字節碼大小遠遠小於機器碼

3.編譯語言&解釋語言

  • 編譯語言

    咱們熟知的C/C++語言,是編譯語言,即程序員編譯以後能夠一步到位(編譯成機器碼),能夠被CPU直接解讀並執行。

    可能有人會問,既然上文中說過字節碼有種種好處,爲何不使用字節碼呢?

    這是由於每種編程語言設計的初衷不一樣,有些是爲了跨平臺而設計的,如Java,但有些是針對某個指定機器或某批指定型號的機器設計的。

    舉個🌰,蘋果公司開發的OC語言和Swift語言,就是針對自家產品設計的,我才無論你其餘人的產品呢。因此OC或Swift語言設計初衷之一就是快,可直接編譯爲機器碼使iPhone或iPad解讀並執行。這也是爲何蘋果手機的應用比安卓手機應用大的主要緣由。這更是爲何蘋果手機更流暢的緣由之一!(沒有中間商賺差價)

  • 編譯-解釋語言

    拿開發Android的語言Java爲例,Java是編譯-解釋語言,即程序員編譯以後不能夠直接編譯爲機器碼,而是會編譯成字節碼(在Java程序中爲.class文件,在Android程序中爲.dex文件)。而後咱們須要將字節碼再解釋成機器碼,使之能被CPU解讀。

    這第二次解釋,即從字節碼解釋成機器碼的過程,是程序安裝或運行後,在Java虛擬機中實現的。

2、形成卡頓的三大因素


今年最新的Android版本已是10了,其實在這兩年關於Android手機卡頓的聲音已經慢慢低了下去,取而代之的是流暢如iOS之類的聲音。

可是諸如超過iOS的話,還比較少,實際上是由於Android有卡頓有三大歷史緣由。起步就比iOS低。

1.虛擬機——解釋過程慢

經過上文描述,咱們能夠知道,iOS之因此不卡是由於他一步到位,省略了中間解釋的步驟,直接跟硬件層進行通訊。而Android因爲沒有一步到位,每次執行都須要實時解釋成機器碼,因此性能較iOS明顯低下。

咱們已經明確知道了字節碼(中間商)是形成卡頓的主要元兇之一,咱們能否像iOS那樣扔掉字節碼,直接一步到位呢?

明顯不能,由於iOS搞來搞去就那麼幾個機型。反觀Android方面,光手機就有無數種機型,無數種CPU架構/型號,更別提什麼平板,車載等其餘設備了。有那麼多類型的硬件設備表明着就有很是多不一樣的硬件架構,每種架構都有本身對應的機器碼解釋規則。顯然像iOS那樣一步到位是不現實的。

那怎麼辦呢?既然扔不掉字節碼這個中間商,那咱們只能剝削他咯,讓整個解釋的過程快一點,再快一點。而解釋所在的「工廠」在虛擬機內。

接下來就是偉大的Android虛擬機進化之路!

① Andorid 1.0 Dalvik(DVM)+解釋器

DVM是Google開發的Android平臺虛擬機,可讀取.dex的字節碼。 上文中所說的從字節碼解釋成機器碼的過程在Java虛擬機中,在Android平臺中虛擬機指的就是這個DVM。 在Android1.0時期,程序一邊運行,DVM中的解釋器(翻譯機)一邊解釋字節碼。 可想而知,這樣效率絕對低下。一個字,卡。

② Android 2.2 DVM+JIT

其實解決DVM的問題思路很清楚,咱們在程序某個功能運行前就解釋就能夠了。

在Android2.2時期,聰明的谷歌引入了JIT(Just In Time)機制,直譯就是即時編譯。

舉個🌰,我常常去一家餐館吃飯,老闆已經知道我想吃什麼菜了,在我到以前就把菜準備好了,這樣我就省去了等菜的時間。

JIT就至關於這個聰明的老闆,它會在手機打開APP時,將用戶常用的功能記下來。當用戶打開APP的時候立馬將這些內容編譯出來,這樣當用戶打開這些內容時,JIT已經將'菜'準備好了。這樣就提升了總體效率。

雖然JIT挺聰明的,且整體思路清晰理想豐滿,但現實是仍然卡的要死。

存在的問題:

  • 打開APP的時候會變慢
  • 每次打開APP都要重複勞動,不能一勞永逸。
  • 若是我忽然點了一盤以前歷來沒點過的菜,那我只好等菜了,因此若是用戶打開了JIT沒有準備好的'菜',就只能等DVM中的解釋器去邊執行邊解釋了。

③ Android 5.0 ART+AOT

聰明的谷歌又想到個方法,既然咱們能在打開APP的時候將字節碼編譯成機器碼,那麼咱們何不在APP安裝的時候就把字節碼編譯成機器碼呢?這樣每次打開APP也不用重複勞動了,一勞永逸。

這確實是個思路,因而谷歌推出了ART來替代DVM,ART全稱Android Runtime,它在DVM的基礎上作了一些優化,它在應用被安裝的時候就將應用編譯成機器碼,這個過程稱爲AOT(Ahead-Of-Time),即預編譯

可是問題又來了,打開APP是不卡了,可是安裝APP慢的要死,可能有人會說,一個APP又不是會頻繁安裝,能夠犧牲下這點時間。 可是很差意思,安卓手機每次OTA啓動(即系統版本更新或刷機後)都會從新安裝全部APP,無奈吧!絕望吧!對,還記得那兩年,被安卓版本更新所支配的恐懼嗎!

④ Android 7.0 混合編譯

谷歌最終祭出了終極大招,DVM+JIT很差,ART+AOT又很差。行,我把他們都混合起來,那總能夠了吧!

因而谷歌在Android7.0的時候,發佈了混合編譯。 即安裝時先不編譯成機器碼,在手機不被使用的時候,AOT偷偷的把能編譯成機器碼的那部分代碼編譯了(至於什麼是能編譯的部分,下文字節碼的編譯模板詳述)。其實就是把以前APP安裝時候乾的活偷偷的在手機空的時候幹了。

若是來不及編譯的話,再把JIT和解釋器這對難兄難弟叫起來,讓他們去編譯或實時解釋。

不得不佩服谷歌這粗暴的解決問題的方式,這樣一來確實Android手機從萬年卡頓慢慢的坑中出來了。

⑤ Android 8.0 改進解釋器

在Android8.0時期,谷歌又盯上了解釋器,其實縱觀上面的問題,根源就是這個解釋器解釋的太慢了!(什麼JIT,AOT,老夫解釋只有一個字,快)那咱們何不讓這個解釋器解釋的快一點呢? 因而谷歌改進了解釋器,解釋模式執行效率大大提高。

⑥ Android 9.0 改進編譯模板

這個點會在下文字節碼的編譯模板中詳述。

這邊簡單而言就是,在Android9.0上提供了預先放置熱點代碼的方式,應用在安裝的時候就能知道經常使用代碼會被提早編譯。(借用知乎@weishu大神的原話)

2.JNI——Java和C互相調用慢

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要一個對象的地址,但萬一這個對象地址被挪動了,那就完蛋了。解決方案有兩個:

    1. 把這個對象在C裏再拷一份。很明顯這形成了很大的開銷。
    2. 告訴ART,我要用這個對象了,GC這個對象的地址你不能動!你先一邊呆着去。這樣相對而言開銷卻是小了,但若是這個地址若是一直不能被回收的話,可能形成OOM。

    (此處參考知乎@張鐸華爲公佈的方舟編譯器到底對安卓軟件生態會有多大影響?中的回答)

3. 字節碼的編譯模板——未針對具體APP進行優化

咱們舉個🌰來理解編譯模版,「Hello world」能夠被翻譯爲「你好,世界」,一樣也能夠被翻譯爲「世界,你好」,這個差異就是編譯模版不一樣致使的,

①. 統一的編譯模版(vm模版)

字節碼能夠經過不一樣的編譯模版被編譯爲機器碼,而編譯模版的不一樣將直接致使編譯完後的機器碼性能截然不同。

在安卓中,ART有一套規定的,統一的編譯模版,暫且稱爲VM模版,這套模版雖算不上差勁,但也算不上優秀。

由於它是谷歌爸爸搞出來的,確定算不上差勁,但因爲沒有針對每個APP進行特定的優化,因此也算不上優秀。

②. vm模版存在的問題

問題就存在於沒有針對每個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並無我說的那麼笨。在以後應用使用過程當中,ART會記錄並學習用戶的使用習慣(保存熱點代碼),而後更新針對當前APP的定製化vm模版,不斷的補充熱點代碼,補充定製化模版

這是否是聽起來很熟悉?在手機發布大會上的宣傳語「基於用戶操做習慣進行學習,APP打開速度不斷提升」的部分原理就是這個。

④. 最終大招,一勞永逸

其實要一勞永逸的解決這個問題思路也不難:咱們只須要在吃飯前跟老闆提早預約想吃啥就行,讓老闆先準備起來,這樣等咱們到了就不用等餐了。

在最新的Android9.0版本中,谷歌推出了這個相似提早預約的功能:編譯系統支持在具備藍圖編譯規則的原生 Android 模塊上使用 Clang 的配置文件引導優化 (PGO)。

說人話:谷歌容許你在開發階段添加一個配置文件,這個配置文件內可指定「熱點代碼」,當應用安裝完後,ART在後臺悄悄編譯APP時,會優先編譯配置文件中指定的「熱點代碼」。

雖然谷歌支持,可是這塊技術對於APP開發人員而言國內資料過於缺少,普及面不廣。筆者先貼上官方連接,以及這篇博客,其中介紹的仍是挺詳細的。(隔壁Xcode針對PGO都有UI界面了)

3、解決思路

解決思路總結爲四個字就是:華爲方舟。

方舟的解決思路:

  1. 針對虛擬機問題,方舟說:我不要你這個爛虛擬機了,咱們裸奔

  2. 針對JNI調用問題,方舟說:咱們讓Java在編譯階段跟C同樣直接編譯成機器碼,幹掉虛擬機,跟.so庫直接調用,毫無JNI開銷問題

  3. 針對編譯模版問題,方舟說:咱們支持針對不一樣APP進行不一樣的編譯優化

總結一下:方舟支持在打包編譯階段針對不一樣APP進行不一樣的編譯優化,而後直接打包成機器碼.apk(極可能已經不叫apk了),而後直接運行。

這樣看起來方舟確實解決掉了三大問題,可是,代價呢?

若是按照這個思路,方舟就確定不止是一個編譯器了,它應該還有一套本身的runtime。固然這些都是後話了。

關於方舟的實現只是大概講了思路,但沒有深刻,由於一來方舟沒開源,二來方舟發佈會PPT營銷層面更多,技術細節缺乏,如今奇思妙想徹底是紙上談兵,一切仍是靜待開源吧。

4、程序員不背卡頓的鍋!

自從發表文章以來,收到了一些反饋,其中有一種聲音是:

形成卡頓的主要緣由是垃圾代碼和保活,全家桶等國產軟件的鍋。

對這一點,我不能否認,垃圾代碼,保活策略,全家桶是很噁心。

可是若是要將這些影響上升爲形成卡頓的主要緣由,

筆者認爲大家是太看得起本身的垃圾代碼負優化能力了,仍是太看不起小米,華爲這些系統生產廠家了,仍是以爲天底下的iOS人手水平高Android一個層次呢?

若是必定要說垃圾代碼形成了卡頓,也請去理解下哪些代碼是所謂的垃圾代碼,好比某些代碼形成了內存抖動和GC頻繁回收形成了卡頓,不要就扔下一句,垃圾代碼而後讓程序員背了全部的鍋。都9102年了,別再隨便甩鍋給程序員了!,也請那些這樣認爲的人別再妄自菲薄了!

至於保活,在如今的華爲小米等系統里弄一個全天候保活,互相拉起的進程,大概就會像黑進阿里的黑客同樣,次日去公司報道吧。

至於一些千元機的卡頓問題,能夠了解下Google新推的Android Go系統,這個系統下的APP開發要求異常的苛刻。

5、參考資料

  1. 華爲公佈的方舟編譯器到底對安卓軟件生態會有多大影響?
  2. 華爲新貴!方舟編譯器的榮光和使命
  3. 一文看懂華爲方舟編譯器,安卓的一大進步
  4. What does a JVM have to do when calling a native method?
  5. 關於Dalvik、ART、DEX、ODEX、JIT、AOT、OAT
相關文章
相關標籤/搜索