網上關於啓動優化的文章多不勝數,內容千篇一概,大都是列舉一些耗時操做,採用異步加載、懶加載等。
而在面試過程當中,關於啓動優化的問題,若是隻是很表面地回答耗時操做應該放在子線程,顯然太過於普通,沒法跟競爭者拉開差距。如何讓面試官知道你的「內功深厚」,那確定是要往原理層面去回答。本文重點仍是關注原理,冷啓動優化這個問題能延伸到不少原理層面的知識點,本文比較有意思的地方是經過反編譯今日頭條App,研究大廠的啓動優化方案。講啓動優化以前,先看下應用的啓動流程html
應用進程不存在的狀況下,從點擊桌面應用圖標,到應用啓動(冷啓動),大概會經歷如下流程:java
整個啓動流程咱們能干預的主要是 4.三、4.5 和6,應用啓動優化主要從這三個地方入手。理想情況下,這三個地方若是不作任何耗時操做,那麼應用啓動速度就是最快的,可是現實很骨感,不少開源庫接入第一步通常都是在Application onCreate方法初始化,有的甚至直接內置ContentProvider,直接在ContentProvider中初始化框架,不給你優化的機會。python
直奔主題,常見的啓動優化方式大概有這些:git
消除啓動時的白屏/黑屏,市面上大部分App都採用了這種方法,很是簡單,是一個障眼法,不會縮短實際冷啓動時間,簡單貼下實現方式吧。github
styles.xml 增長一個主題叫AppThemeWelcome面試
閃屏頁設置這個主題,或者全局給Application設置數據庫
這樣的話啓動Activity以後背景會一直在,因此在Activity的onCreate方法中切換成正常主題數組
這樣打開桌面圖標會立刻顯示logo,不會出現黑/白屏,直到Activity啓動完成,替換主題,logo消失,可是總的啓動時間並無改變。瀏覽器
說MultiDex以前,先梳理下apk編譯流程
Android Studio 按下編譯按鈕後發生了什麼?
在第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。
在Android 4.4的機器打印MultiDex.install(context)耗時以下:
平均耗時1秒以上,目前大部分應用應該仍是會兼容5.0如下手機,那麼MultiDex優化是冷啓動優化的大頭。爲何MultiDex會這麼耗時?老規矩,分析一下MultiDex原理~
下面看下MultiDex的install 方法作了什麼事
從入口的判斷來看,若是虛擬機自己就支持加載多個dex文件,那就啥都不用作;若是是不支持加載多個dex(5.0如下是不支持的),則走到 doInstallation 方法。
先看註釋1,MultiDexExtractor#load
查找dex文件,有兩個邏輯,有緩存就調用loadExistingExtractions方法,沒有緩存或者緩存讀取失敗就調用performExtractions方法,而後再緩存起來。使用到緩存,那麼performExtractions 方法想必應該是很耗時的,分析一下代碼:
這裏的邏輯就是解壓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 以上的實現
就是建立一個新的數組,把原來數組內容(主dex)和要增長的內容(dex二、dex3…)拷貝進去,反射替換原來的dexElements爲新的數組,以下圖
看起來有點眼熟,Tinker熱修復的原理也是經過反射將修復後的dex添加到這個dex數組去,不一樣的是熱修復是添加到數組最前面,而MultiDex是添加到數組後面。這樣講可能還不是很好理解?來看看ClassLoader怎麼加載一個類的就明白了~
無論是 PathClassLoader仍是DexClassLoader,都繼承自BaseDexClassLoader,加載類的代碼在 BaseDexClassLoader中4.4 源碼/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
接着看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添加進去呢? 答案就是反射DexPathList的dexElements字段,而後把咱們的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,本身寫了一套,猜測多是優化這個多餘的壓縮過程,頭條的方案下面會介紹。
ClassLoader 加載類原理:
ClassLoader.loadClass -> DexPathList.loadClass -> 遍歷dexElements數組 ->DexFile.loadClassBinaryName
通俗點說就是:ClassLoader加載類的時候是經過遍歷dex數組,從dex文件裏面去加載一個類,加載成功就返回,加載失敗則拋出Class Not Found 異常。MultiDex原理:
在明白ClassLoader加載類原理以後,咱們能夠經過反射dexElements數組,將新增的dex添加到數組後面,這樣就保證ClassLoader加載類的時候能夠重新增的dex中加載到目標類,通過分析後最終MultipDex原理圖以下:
知道了MultiDex原理以後,能夠理解install過程爲何耗時,由於涉及到解壓apk取出dex、壓縮dex、將dex文件經過反射轉換成DexFile對象、反射替換數組。那麼MultiDex到底應該怎麼優化呢,放子線程可行嗎?
這個方法你們很容易就能想到,在閃屏頁開一個子線程去執行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了。因此這種方案的缺點很明顯:
- MultiDex加載邏輯放在閃屏頁的話,閃屏頁中引用到的類都要配置在主dex。
- ContentProvider必須在主dex,一些第三方庫自帶ContentProvider,維護比較麻煩,要一個一個配置。
這時候就思考一下,有沒有其它更好的方案呢?大廠是怎麼作的?今日頭條確定要對MultiDex進行優化吧,反編譯瞧瞧?
點了一根菸以後,開始偷代碼…
今日頭條沒有加固,反編譯後很容易經過關鍵字搜索找到MultidexApplication這個類,
看註釋1的d.a(this);這個方法,代碼雖然混淆了,可是方法內部的代碼仍是能夠看出是幹嗎的,繼續跟這個方法,爲了避免影響閱讀,我對混淆作了一些處理,改爲正常的方法名。
每一個方法開頭都有PatchProxy.isSupport這個if判斷,這個是美團Robust熱修復生成的代碼,今日頭條沒有本身的熱修復框架,沒有用Tinker,而是用美團的,想了解關於Robust細節能夠參考文末連接。Robust直接跳過,看else代碼塊便可。繼續看loadMultiDex方法
邏輯以下:
1. 建立臨時文件,做爲判斷MultiDex是否加載完的條件
2. 啓動LoadDexActivity去加載MultiDex(LoadDexActivity在單獨進程),加載完會刪除臨時文件
3. 開啓while循環,直到臨時文件不存在才跳出循環,進入Application的onCreate建立臨時文件代碼
while循環代碼
LoadDexActivity 只有一個加載框,加載完再跳轉到閃屏頁
dex加載完應該要finish掉當前Activity
按照上面代碼分析,今日頭條在5.0如下手機首次啓動應該是這樣:
其實是不是這樣呢,用4.4機器試下?
看起來徹底跟猜測的一致,擼幾行代碼驗證應該不難吧?
點了一根菸以後,開始擼代碼,最終實現效果以下
效果跟今日頭條是一致的,再也不重複分析代碼了,源碼上傳到github,感興趣的同窗能夠參考參考,頭條的方案,值得嘗試~ github.com/lanshifu/Mu…再次梳理一下這種方式:
有些同窗可能會問,啓動仍是好久啊,冷啓動時間有變化嗎?冷啓動時間是指點擊桌面圖標到第一個Activity顯示這段時間。
方案1:直接在閃屏頁開個子線程去執行MultiDex邏輯,MultiDex不影響冷啓動速度,可是難維護。方案2:今日頭條的MultiDex優化方案:
其實應該還有方案3,由於我發現頭條並無直接使用Google的MultiDex,而是參考谷歌的MultiDex,本身寫了一套,耗時應該會少一些,你們有興趣能夠去研究一下。
這段代碼是今日頭條裏面的,Activity對象預先new出來,
對象第一次建立的時候,java虛擬機首先檢查類對應的Class 對象是否已經加載。若是沒有加載,jvm會根據類名查找.class文件,將其Class對象載入。同一個類第二次new的時候就不須要加載類對象,而是直接實例化,建立時間就縮短了。
頭條真是把啓動優化作到極致。
不少第三方開源庫都說在Application中進行初始化,十幾個開源庫都放在Application中,確定對冷啓動會有影響,因此能夠考慮按需初始化,例如Glide,能夠放在本身封裝的圖片加載類中,調用到再初始化,其它庫也是同理,讓Application變得更輕。
WebView啓動優化文章也比較多,這裏只說一下大概優化思路。
這一部分能夠參考:mp.weixin.qq.com/s/KwvWURD5W…
這種方式通常是在主頁空閒的時候,將其它頁面的數據加載好,保存到內存或數據庫,等到打開該頁面的時候,判斷已經預加載過,直接從內存或數據庫讀取數據並顯示。
線程是程序運行的基本單位,線程的頻繁建立是耗性能的,因此你們應該都會用線程池。單個cpu狀況下,即便是開多個線程,同時也只有一個線程能夠工做,因此線程池的大小要根據cpu個數來肯定。啓動優化方式就先介紹到這裏,常見的就是這些,其它的能夠做爲補充。
Android學習PDF+架構視頻+面試文檔+源碼筆記
TraceView性能損耗太大,獲得的結果不真實。 Systrace 能夠方便追蹤關鍵系統調用的耗時狀況,如 Choreographer,可是不支持應用程序代碼的耗時分析。
結合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,直接能夠用谷歌瀏覽器打開查看。
BlockCanary 能夠監聽主線程耗時的方法,將閾值設置低一點,好比200毫秒,這樣的話若是一個方法執行時間超過200毫秒,獲取堆棧信息並通知開發者。BlockCanary 原理在以前那篇卡頓優化的文章裏面講過一些,這裏就再也不重複。
文章有點長,看到這裏,是否是忘記開頭講什麼了?總結一下這篇文章主要涉及到哪些內容:
面試問到啓動優化問題,不要簡單一兩句話回答,能夠說說本身在實際項目中作了哪些優化,好比Multidex優化,把整個流程,原理說清楚。固然,前提是本身要去實踐,理解爲何要這樣作。
阿里P6P7【安卓】進階資料分享+加薪跳槽必備面試題