為什么要分包?
1、65536問題
-
導(dǎo)致因素
隨著項(xiàng)目apk的龐大以及加入更多的第三方庫,app的方法數(shù)已經(jīng)超過了65536,會(huì)導(dǎo)致程序根本跑不起來。
-
原因
在生成.dex文件后由于有很多冗余的資源,所以Android中會(huì)對(duì)dex文件進(jìn)行優(yōu)化,Davlik模式下利用dexopt工具進(jìn)行優(yōu)化,而dexopt有兩個(gè)問題:-
Dexopt會(huì)把每一個(gè)類的方法 id 檢索起來,存在一個(gè)鏈表結(jié)構(gòu)里面,但是這個(gè)鏈表的長(zhǎng)度是用一個(gè)short 類型來保存的,導(dǎo)致了方法 id 的數(shù)目不能夠超過65536個(gè), 當(dāng)一個(gè)項(xiàng)目足夠大的時(shí)候,顯然這個(gè)方法數(shù)的上限是不夠的; - Dexopt 使用
LinearAlloc來存儲(chǔ)應(yīng)用的方法信息, Dalvik LinearAlloc 是一個(gè)固定大小的緩沖區(qū)。在Android 版本的歷史上,LinearAlloc 分別經(jīng)歷了4M/5M/8M/16M限制。Android 2.2和2.3的緩沖區(qū)只有5MB,Android 4.x提高到了8MB 或16MB。當(dāng)方法數(shù)量過多導(dǎo)致超出緩沖區(qū)大小時(shí),也會(huì)造成dexopt崩潰;
-
在
ART模式下 ,采用的是dexoat工具,對(duì)應(yīng)生成art虛擬執(zhí)行可執(zhí)行的.oat文件,這個(gè)是包含多個(gè)dex文件;
2、怎么解決這個(gè)問題
- 在gradle中添加MultiDex支持,加載classes2.dex
multiDexEnabled true
- 執(zhí)行MultiDex.install()
@Override protected void attachBaseContext (Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
分包導(dǎo)致的問題
API14 之前的不能支持分包Dalvik linearalloc bug在冷啟動(dòng)時(shí)因?yàn)樾枰惭bdex文件,如果dex文件過大時(shí),處理時(shí)間過長(zhǎng),很容易引發(fā)ANR(Application Not Responding);
采用MultiDex方案的應(yīng)用因?yàn)樾枰暾?qǐng)一個(gè)很大的內(nèi)存,在運(yùn)行時(shí)可能導(dǎo)致程序的崩潰,這個(gè)主要是因?yàn)镈alvik linearAlloc 的一個(gè)限制,這個(gè)限制在 Android 4.0 (API level 14)已經(jīng)增加了, 應(yīng)用也有可能在低于 Android 5.0 (API level 21)版本的機(jī)器上觸發(fā)這個(gè)限制;
分包后,不同依賴項(xiàng)目間的dex文件函數(shù)相互調(diào)用,
報(bào)錯(cuò)找不到方法
Android系統(tǒng)對(duì)分包的影響
Android 5.0以下:
運(yùn)行在Davlik虛擬機(jī)上,優(yōu)化使用dexopt工具并分包,每次運(yùn)行先加載主包,然后反射子包,存在主包子包的先后問題;Android 5.0以上:
運(yùn)行在ART虛擬機(jī)上,優(yōu)化使用dexoat工具,生成多個(gè)包含dex文件的.oat文件,.oat文件是混合了主包子包,已經(jīng)在APK安裝時(shí)生成,故程序運(yùn)行起來不存在主包子包的加載先后問題;
MultiDex的基本原理
通過DexFile來加載Secondary DEX,并存放在BaseDexClassLoader的DexPathList中。
解決分包導(dǎo)致調(diào)用找不到對(duì)應(yīng)類
1、微信加載方案
首次加載在地球中頁中, 并用線程去加載(但是 5.0 之前加載 dex 時(shí)還是會(huì)掛起主線程一段時(shí)間(不是全程都掛起))。
dex 形式
微信是將包放在assets目錄下的,在加載 Dex 的代碼時(shí),實(shí)際上傳進(jìn)去的是 zip,在加載前需要驗(yàn)證 MD5,確保所加載的 Dex 沒有被篡改。dex 類分包規(guī)則
分包規(guī)則即將所有 Application、ContentProvider 以及所有 export 的 Activity、Service 、Receiver 的間接依賴集都必須放在主 dex。加載 dex 的方式
加載邏輯這邊主要判斷是否已經(jīng) dexopt,若已經(jīng) dexopt,即放在 attachBaseContext 加載,反之放于地球中用線程加載。怎么判斷?因?yàn)樵谖⑿胖?,若判?revision 改變,即將 dex 以及 dexopt 目錄清空。只需簡(jiǎn)單判斷兩個(gè)目錄 dex 名稱、數(shù)量是否與配置文件的一致。
總的來說,這種方案用戶體驗(yàn)較好,缺點(diǎn)在于太過復(fù)雜,每次都需重新掃描依賴集,而且使用的是比較大的間接依賴集。
2、 Facebook 加載方案
Facebook的思路是將 MultiDex.install() 操作放在另外一個(gè)經(jīng)常進(jìn)行的。
dex 形式
與微信相同。dex 類分包規(guī)則
Facebook 將加載 dex 的邏輯單獨(dú)放于一個(gè)單獨(dú)的 nodex 進(jìn)程中。
<activity
android:exported="false"
android:process=":nodex"
android:name="com.facebook.nodex.startup.splashscreen.NodexSplashActivity">
所有的依賴集為 Application、NodexSplashActivity 的間接依賴集即可。
- 加載 dex 的方式
因?yàn)?NodexSplashActivity的 intent-filter 指定為Main和LAUNCHER,所以一打開 App 首先拉起 nodex 進(jìn)程,然后打開NodexSplashActivity進(jìn)行MultiDex.install()。如果已經(jīng)進(jìn)行了 dexpot 操作的話就直接跳轉(zhuǎn)主界面,沒有的話就等待 dexpot 操作完成再跳轉(zhuǎn)主界面。
這種方式好處在于依賴集非常簡(jiǎn)單,同時(shí)首次加載 dex 時(shí)也不會(huì)卡死。但是它的缺點(diǎn)也很明顯,即每次啟動(dòng)主進(jìn)程時(shí),都需先啟動(dòng) nodex 進(jìn)程。盡管 nodex 進(jìn)程邏輯非常簡(jiǎn)單,這也需100ms以上。
3、美團(tuán)加載方案
- dex 形式
在 gradle 生成 dex 文件的這步中,自定義一個(gè) task 來干預(yù) dex 的生產(chǎn)過程,從而產(chǎn)生多個(gè) dex 。
tasks.whenTaskAdded { task ->
if (task.name.startsWith('proguard') && (task.name.endsWith('Debug') || task.name.endsWith('Release'))) {
task.doLast {
makeDexFileAfterProguardJar();
}
task.doFirst {
delete "${project.buildDir}/intermediates/classes-proguard";
String flavor = task.name.substring('proguard'.length(), task.name.lastIndexOf(task.name.endsWith('Debug') ? "Debug" : "Release"));
generateMainIndexKeepList(flavor.toLowerCase());
}
} else if (task.name.startsWith('zipalign') && (task.name.endsWith('Debug') || task.name.endsWith('Release'))) {
task.doFirst {
ensureMultiDexInApk();
}
}
}
-
dex 類分包規(guī)則
把 Service、Receiver、Provider 涉及到的代碼都放到主 dex 中,而把 Activity 涉及到的代碼進(jìn)行了一定的拆分,把首頁 Activity、Laucher Activity 、歡迎頁的 Activity 、城市列表頁 Activity 等所依賴的 class 放到了主 dex 中,把二級(jí)、三級(jí)頁面的 Activity 以及業(yè)務(wù)頻道的代碼放到了第二個(gè) dex 中,為了減少人工分析 class 的依賴所帶了的不可維護(hù)性和高風(fēng)險(xiǎn)性,美團(tuán)編寫了一個(gè)能夠自動(dòng)分析 class 依賴的腳本, 從而能夠保證?主 dex 包含 class 以及他們所依賴的所有 class 都在其內(nèi),這樣這個(gè)腳本就會(huì)在打包之前自動(dòng)分析出啟動(dòng)到主 dex 所涉及的所有代碼,保證主 dex 運(yùn)行正常。
加載 dex 的方式
通過分析 Activity 的啟動(dòng)過程,發(fā)現(xiàn) Activity 是由 ActivityThread 通過 Instrumentation 來啟動(dòng)的,那么是否可以在 Instrumentation 中做一定的手腳呢?通過分析代碼 ActivityThread 和 Instrumentation 發(fā)現(xiàn),Instrumentation 有關(guān) Activity 啟動(dòng)相關(guān)的方法大概有:execStartActivity、 newActivity 等等,這樣就可以在這些方法中添加代碼邏輯進(jìn)行判斷這個(gè) class 是否加載了,如果加載則直接啟動(dòng)這個(gè) Activity,如果沒有加載完成則啟動(dòng)一個(gè)等待的 Activity 顯示給用戶,然后在這個(gè) Activity 中等待后臺(tái)第二個(gè) dex 加載完成,完成后自動(dòng)跳轉(zhuǎn)到用戶實(shí)際要跳轉(zhuǎn)的 Activity;這樣在代碼充分解耦合,以及每個(gè)業(yè)務(wù)代碼能夠做到顆?;那疤嵯?,就做到第二個(gè) dex 的按需加載了。
美團(tuán)的這種方式對(duì)主 dex 的要求非常高,因?yàn)榈诙€(gè) dex 是等到需要的時(shí)候再去加載。重寫Instrumentation 的 execStartActivity 方法,hook 跳轉(zhuǎn) Activity 的總?cè)肟谧雠袛啵绻?dāng)前第二個(gè) dex 還沒有加載完成,就彈一個(gè) loading Activity等待加載完成。
最后,希望此篇博客對(duì)大家有所幫助,歡迎提出問題及建議共同探討,如有興趣可以關(guān)注我的博客,謝謝!