今日頭條APP啓動很快,原來是作了這些優化?

前言

網上關於啓動優化的文章多不勝數,內容千篇一概,大都是列舉一些耗時操做,採用異步加載、懶加載等。

阿里P6P7【安卓】進階資料分享+加薪跳槽必備面試題css

而在面試過程當中,關於啓動優化的問題,若是隻是很表面地回答耗時操做應該放在子線程,顯然太過於普通,沒法跟競爭者拉開差距。如何讓面試官知道你的「內功深厚」,那確定是要往原理層面去回答。本文重點仍是關注原理,冷啓動優化這個問題能延伸到不少原理層面的知識點,本文比較有意思的地方是經過反編譯今日頭條App,研究大廠的啓動優化方案。講啓動優化以前,先看下應用的啓動流程html

1、應用啓動流程

應用進程不存在的狀況下,從點擊桌面應用圖標,到應用啓動(冷啓動),大概會經歷如下流程:java

  1. Launcher startActivity
  2. AMS startActivity
  3. Zygote fork 進程
  4. ActivityThread main()
    4.1. ActivityThread attach
    4.2. handleBindApplication
    4.3 attachBaseContext
    4.4. installContentProviders
    4.5. Application onCreate
  5. ActivityThread 進入loop循環
  6. Activity生命週期回調,onCreate、onStart、onResume…

整個啓動流程咱們能干預的主要是 4.三、4.5 和6,應用啓動優化主要從這三個地方入手。理想情況下,這三個地方若是不作任何耗時操做,那麼應用啓動速度就是最快的,可是現實很骨感,不少開源庫接入第一步通常都是在Application onCreate方法初始化,有的甚至直接內置ContentProvider,直接在ContentProvider中初始化框架,不給你優化的機會。python

2、啓動優化

直奔主題,常見的啓動優化方式大概有這些:git

  • 閃屏頁優化
  • MultipDex優化(本文重點)
  • 第三方庫懶加載
  • WebView優化
  • 線程優化
  • 系統調用優化

2.1 閃屏頁優化

消除啓動時的白屏/黑屏,市面上大部分App都採用了這種方法,很是簡單,是一個障眼法,不會縮短實際冷啓動時間,簡單貼下實現方式吧。github

styles.xml 增長一個主題叫AppThemeWelcome面試

閃屏頁設置這個主題,或者全局給Application設置數據庫

這樣的話啓動Activity以後背景會一直在,因此在Activity的onCreate方法中切換成正常主題數組

這樣打開桌面圖標會立刻顯示logo,不會出現黑/白屏,直到Activity啓動完成,替換主題,logo消失,可是總的啓動時間並無改變。瀏覽器

2.2 MultiDex 優化(本文重點)

MultiDex以前,先梳理下apk編譯流程

2.2.1 apk編譯流程

Android Studio 按下編譯按鈕後發生了什麼?

  1. 打包資源文件,生成R.java文件(使用工具AAPT)
  2. 處理AIDL文件,生成java代碼(沒有AIDL則忽略)
  3. 編譯 java 文件,生成對應.class文件(java compiler)
  4. .class 文件轉換成dex文件(dex)
  5. 打包成沒有簽名的apk(使用工具apkbuilder)
  6. 使用簽名工具給apk簽名(使用工具Jarsigner)
  7. 對簽名後的.apk文件進行對齊處理,不進行對齊處理不能發佈到Google Market(使用工具zipalign)

在第4步,將class文件轉換成dex文件,默認只會生成一個dex文件,單個dex文件中的方法數不能超過65536,否則編譯會報錯:

Unable to execute dex: method ID not in [0, 0xffff]: 65536

App集成一堆庫以後,方法數通常都是超過65536的,解決辦法就是:一個dex裝不下,用多個dex來裝,gradle增長一行配置便可。

multiDexEnabled true

這樣解決了編譯問題,在5.0以上手機運行正常,可是5.0如下手機運行直接crash,報錯 Class NotFound xxx。Android 5.0如下,ClassLoader加載類的時候只會從class.dex(主dex)里加載,ClassLoader不認識其它的class2.dex、class3.dex、…,當訪問到不在主dex中的類的時候,就會報錯:Class NotFound xxx,所以谷歌給出兼容方案,MultiDex

2.2.2 MultiDex 原來這麼耗時

在Android 4.4的機器打印MultiDex.install(context)耗時以下:

平均耗時1秒以上,目前大部分應用應該仍是會兼容5.0如下手機,那麼MultiDex優化是冷啓動優化的大頭。爲何MultiDex會這麼耗時?老規矩,分析一下MultiDex原理~

2.2.3 MultiDex 原理

下面看下MultiDex的install 方法作了什麼事

從入口的判斷來看,若是虛擬機自己就支持加載多個dex文件,那就啥都不用作;若是是不支持加載多個dex(5.0如下是不支持的),則走到 doInstallation 方法。

先看註釋1,MultiDexExtractor#loadimage

查找dex文件,有兩個邏輯,有緩存就調用loadExistingExtractions方法,沒有緩存或者緩存讀取失敗就調用performExtractions方法,而後再緩存起來。使用到緩存,那麼performExtractions 方法想必應該是很耗時的,分析一下代碼:image

這裏的邏輯就是解壓apk,遍歷出裏面的dex文件,例如class1.dex,class2.dex,而後又壓縮成class1.zip,class2.zip…,而後返回zip文件列表。思考爲何這裏要壓縮呢?後面涉及到ClassLoader加載類原理的時候會分析ClassLoader支持的文件格式。第一次加載纔會執行解壓和壓縮過程,第二次進來讀取sp中保存的dex信息,直接返回file list,因此第一次啓動的時候比較耗時。dex文件列表找到了,回到上面MultiDex#doInstallation方法的註釋2,找到的dex文件列表,而後調用installSecondaryDexes方法進行安裝,怎麼安裝呢?方法點進去看SDK 19 以上的實現

  1. 反射ClassLoader 的 pathList 字段
  2. 找到pathList 字段對應的類的makeDexElements 方法
  3. 經過MultiDex.expandFieldArray 這個方法擴展 dexElements 數組,怎麼擴展?看下代碼:

就是建立一個新的數組,把原來數組內容(主dex)和要增長的內容(dex二、dex3…)拷貝進去,反射替換原來的dexElements爲新的數組,以下圖

看起來有點眼熟,Tinker熱修復的原理也是經過反射將修復後的dex添加到這個dex數組去,不一樣的是熱修復是添加到數組最前面,而MultiDex是添加到數組後面。這樣講可能還不是很好理解?來看看ClassLoader怎麼加載一個類的就明白了~

2.2.4 ClassLoader 加載類原理

無論是 PathClassLoader仍是DexClassLoader,都繼承自BaseDexClassLoader,加載類的代碼在 BaseDexClassLoader4.4 源碼/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java

  1. 構造方法經過傳入dex路徑,建立了DexPathList
  2. ClassLoader的findClass方法最終是調用DexPathList 的findClass方法

接着看DexPathList源碼 /dalvik/src/main/java/dalvik/system/DexPathList.java

DexPathList裏面定義了一個dexElements 數組,findClass方法中用到,看下

findClass方法邏輯很簡單,就是遍歷dexElements 數組,拿到裏面的DexFile對象,經過DexFile的loadClassBinaryName方法加載一個類。

最終建立Class是經過native方法,就不追下去了,你們有興趣能夠看下native層是怎麼建立Class對象的。DexFile.cpp那麼問題來了,5.0如下這個dexElements 裏面只有主dex(能夠認爲是一個bug),沒有dex二、dex3…,MultiDex是怎麼把dex2添加進去呢? 答案就是反射DexPathListdexElements字段,而後把咱們的dex2添加進去,固然,dexElements裏面放的是Element對象,咱們只有dex2的路徑,必須轉換成Element格式才行,因此反射DexPathList裏面的makeDexElements 方法,將dex文件轉換成Element對象便可。

dex二、dex3…經過makeDexElements方法轉換成要新增的Element數組,最後一步就是反射DexPathList的dexElements字段,將原來的Element數組和新增的Element數組合並,而後反射賦值給dexElements變量,最後DexPathList的dexElements變量就包含咱們新加的dex在裏面了。makeDexElements方法會判斷file類型,上面講dex提取的時候解壓apk獲得dex,而後又將dex壓縮成zip,壓縮成zip,就會走到第二個判斷裏去。仔細想一想,其實dex不壓縮成zip,走第一個判斷也沒啥問題吧,那谷歌的MultiDex爲何要將dex壓縮成zip呢?在Android開發高手課中看到張紹文也提到這一點

而後我在反編譯頭條App的時候,發現頭條參考谷歌的MultiDex,本身寫了一套,猜測多是優化這個多餘的壓縮過程,頭條的方案下面會介紹。

2.2.5 原理小結

ClassLoader 加載類原理:

ClassLoader.loadClass -> DexPathList.loadClass -> 遍歷dexElements數組 ->DexFile.loadClassBinaryName

通俗點說就是:ClassLoader加載類的時候是經過遍歷dex數組,從dex文件裏面去加載一個類,加載成功就返回,加載失敗則拋出Class Not Found 異常。MultiDex原理:

在明白ClassLoader加載類原理以後,咱們能夠經過反射dexElements數組,將新增的dex添加到數組後面,這樣就保證ClassLoader加載類的時候能夠重新增的dex中加載到目標類,通過分析後最終MultipDex原理圖以下:

2.2.6 MultiDex 優化(兩種方案)

知道了MultiDex原理以後,能夠理解install過程爲何耗時,由於涉及到解壓apk取出dex、壓縮dex、將dex文件經過反射轉換成DexFile對象、反射替換數組。那麼MultiDex到底應該怎麼優化呢,放子線程可行嗎?

方案1:子線程install(不推薦)

這個方法你們很容易就能想到,在閃屏頁開一個子線程去執行MultiDex.install,而後加載完才跳轉到主頁。須要注意的是閃屏頁的Activity,包括閃屏頁中引用到的其它類必須在主dex中,否則在MultiDex.install以前加載這些不在主dex中的類會報錯Class Not Found。這個能夠經過gradle配置,以下:

maindexlist.txt 文件指定哪些類要打包到主dex中,內容格式以下

在已有項目中用這種方式,一頓操做猛如虎以後,編譯運行在4.4的機器上,啓動閃屏頁,加載完準備進入主頁直接崩掉了。

報錯NoClassDefFoundError,通常都是該類沒有在主dex中,要在maindexlist.txt 將配置指定在主dex。 第三方庫中的ContentProvider必須指定在主dex中,不然也會找不到,爲何?文章開頭說過應用的啓動流程,ContentProvider 初始化時機以下圖:

ContentProvider初始化太早了,若是不在主dex中,還沒啓動閃屏頁就已經crash了。因此這種方案的缺點很明顯:

  1. MultiDex加載邏輯放在閃屏頁的話,閃屏頁中引用到的類都要配置在主dex。
  2. ContentProvider必須在主dex,一些第三方庫自帶ContentProvider,維護比較麻煩,要一個一個配置。

這時候就思考一下,有沒有其它更好的方案呢?大廠是怎麼作的?今日頭條確定要對MultiDex進行優化吧,反編譯瞧瞧?

點了一根菸以後,開始偷代碼…

MultiDex優化方案2:今日頭條方案

今日頭條沒有加固,反編譯後很容易經過關鍵字搜索找到MultidexApplication這個類,

看註釋1的d.a(this);這個方法,代碼雖然混淆了,可是方法內部的代碼仍是能夠看出是幹嗎的,繼續跟這個方法,爲了避免影響閱讀,我對混淆作了一些處理,改爲正常的方法名。

每一個方法開頭都有PatchProxy.isSupport這個if判斷,這個是美團Robust熱修復生成的代碼,今日頭條沒有本身的熱修復框架,沒有用Tinker,而是用美團的,想了解關於Robust細節能夠參考文末連接。Robust直接跳過,看else代碼塊便可。繼續看loadMultiDex方法image

邏輯以下:
1. 建立臨時文件,做爲判斷MultiDex是否加載完的條件
2. 啓動LoadDexActivity去加載MultiDex(LoadDexActivity在單獨進程),加載完會刪除臨時文件
3. 開啓while循環,直到臨時文件不存在才跳出循環,進入Application的onCreate建立臨時文件代碼

while循環代碼

LoadDexActivity 只有一個加載框,加載完再跳轉到閃屏頁image

dex加載完應該要finish掉當前Activity

按照上面代碼分析,今日頭條在5.0如下手機首次啓動應該是這樣:

  1. 打開桌面圖標
  2. 顯示默認背景
  3. 跳轉到加載dex的界面,展現一個loading的加載框幾秒鐘
  4. 跳轉到閃屏頁

其實是不是這樣呢,用4.4機器試下?

看起來徹底跟猜測的一致,擼幾行代碼驗證應該不難吧?

點了一根菸以後,開始擼代碼,最終實現效果以下image

效果跟今日頭條是一致的,再也不重複分析代碼了,源碼上傳到github,感興趣的同窗能夠參考參考,頭條的方案,值得嘗試~ github.com/lanshifu/Mu…再次梳理一下這種方式:

  1. 在主進程Application 的 attachBaseContext 方法中判斷若是須要使用MultiDex,則建立一個臨時文件,而後開一個進程(LoadDexActivity),顯示Loading,異步執行MultiDex.install 邏輯,執行完就刪除臨時文件並finish本身。
  2. 主進程Application 的 attachBaseContext 進入while代碼塊,定時輪循臨時文件是否被刪除,若是被刪除,說明MultiDex已經執行完,則跳出循環,繼續正常的應用啓動流程。
  3. 注意LoadDexActivity 必需要配置在main dex中。

有些同窗可能會問,啓動仍是好久啊,冷啓動時間有變化嗎?冷啓動時間是指點擊桌面圖標到第一個Activity顯示這段時間。

MultiDex優化總結

方案1:直接在閃屏頁開個子線程去執行MultiDex邏輯,MultiDex不影響冷啓動速度,可是難維護。方案2:今日頭條的MultiDex優化方案:

  1. 在Application 的attachBaseContext 方法裏,啓動另外一個進程的LoadDexActivity去異步執行MultiDex邏輯,顯示Loading。
  2. 而後主進程Application進入while循環,不斷檢測MultiDex操做是否完成
  3. MultiDex執行完以後主進程Application繼續走,ContentProvider初始化和Application onCreate方法,也就是執行主進程正常的邏輯。

其實應該還有方案3,由於我發現頭條並無直接使用Google的MultiDex,而是參考谷歌的MultiDex,本身寫了一套,耗時應該會少一些,你們有興趣能夠去研究一下。

2.3 預建立Activity

這段代碼是今日頭條裏面的,Activity對象預先new出來,

對象第一次建立的時候,java虛擬機首先檢查類對應的Class 對象是否已經加載。若是沒有加載,jvm會根據類名查找.class文件,將其Class對象載入。同一個類第二次new的時候就不須要加載類對象,而是直接實例化,建立時間就縮短了。

頭條真是把啓動優化作到極致。

2.4 第三方庫懶加載

不少第三方開源庫都說在Application中進行初始化,十幾個開源庫都放在Application中,確定對冷啓動會有影響,因此能夠考慮按需初始化,例如Glide,能夠放在本身封裝的圖片加載類中,調用到再初始化,其它庫也是同理,讓Application變得更輕。

2.5 WebView啓動優化。

WebView啓動優化文章也比較多,這裏只說一下大概優化思路。

  1. WebView第一次建立比較耗時,能夠預先建立WebView,提早將其內核初始化。
  2. 使用WebView緩存池,用到WebView的地方都從緩存池取,緩存池中沒有緩存再建立,注意內存泄漏問題。
  3. 本地預置html和css,WebView建立的時候先預加載本地html,以後經過js腳本填充內容部分。

這一部分能夠參考:mp.weixin.qq.com/s/KwvWURD5W…

2.6 數據預加載

這種方式通常是在主頁空閒的時候,將其它頁面的數據加載好,保存到內存或數據庫,等到打開該頁面的時候,判斷已經預加載過,直接從內存或數據庫讀取數據並顯示。

2.7 線程優化

線程是程序運行的基本單位,線程的頻繁建立是耗性能的,因此你們應該都會用線程池。單個cpu狀況下,即便是開多個線程,同時也只有一個線程能夠工做,因此線程池的大小要根據cpu個數來肯定。啓動優化方式就先介紹到這裏,常見的就是這些,其它的能夠做爲補充。
Android學習PDF+架構視頻+面試文檔+源碼筆記

3、啓動耗時分析方法

TraceView性能損耗太大,獲得的結果不真實。 Systrace 能夠方便追蹤關鍵系統調用的耗時狀況,如 Choreographer,可是不支持應用程序代碼的耗時分析。

3.1 Systrace + 函數插樁

結合Systrace 和 函數插樁,就是將以下代碼插入到每一個方法的入口和出口

插樁後的代碼以下

插樁工具參考:github.com/AndroidAdva…mac下systrace路徑在

/Users/{xxx}/Library/Android/sdk/platform-tools/systrace/

編譯運行app,執行命令

python2 

/Users/lanshifu/Library/Android/sdk/platform-tools/systrace/systrace.py gfx view wm am pm ss dalvik app sched -b 90960 -a com.sample.systrace -o test.log.html

最後按下Enter中止捕獲trace信息,在目錄下生成報告test.log.html,直接能夠用谷歌瀏覽器打開查看。

3.2 BlockCanary 也能夠檢測

BlockCanary 能夠監聽主線程耗時的方法,將閾值設置低一點,好比200毫秒,這樣的話若是一個方法執行時間超過200毫秒,獲取堆棧信息並通知開發者。BlockCanary 原理在以前那篇卡頓優化的文章裏面講過一些,這裏就再也不重複。

總結

文章有點長,看到這裏,是否是忘記開頭講什麼了?總結一下這篇文章主要涉及到哪些內容:

  1. 應用啓動流程
  2. 閃屏頁優化
  3. MultiDex 原理分析
  4. ClassLoader 加載一個類的流程分析
  5. 熱修復原理
  6. MultiDex優化:介紹了兩種方式,一種是直接在閃屏頁開個子線程去加載dex,難維護,不推薦;一種是今日頭條的方案,在單獨一個進程加載dex,加載完主進程再繼續。
  7. 快速啓動Activity的方式:預建立Activity,預加載數據。
  8. 啓動時間監控的方式:Systrace+插樁、BlockCanary。

面試問到啓動優化問題,不要簡單一兩句話回答,能夠說說本身在實際項目中作了哪些優化,好比Multidex優化,把整個流程,原理說清楚。固然,前提是本身要去實踐,理解爲何要這樣作。
阿里P6P7【安卓】進階資料分享+加薪跳槽必備面試題

相關文章
相關標籤/搜索