一缰冤、背景
隨著業(yè)務(wù)規(guī)模發(fā)展,不斷的加入新的功能碎节,添加新的類庫,app的方法數(shù)已經(jīng)超過65535,這樣的情況下就會(huì)遇到以下這個(gè)錯(cuò)誤
導(dǎo)致app無法安裝处坪,開發(fā)無法進(jìn)行。
具體的原因是在早期的 Android 系統(tǒng)中架专,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崩潰。
盡管在新版本的 Android 系統(tǒng)中钞支,DexOpt 修復(fù)了方法數(shù)65K的限制問題茫蛹,并且擴(kuò)大了 LinearAlloc 限制,但是我們?nèi)匀恍枰獙?duì)低版本的 Android 系統(tǒng)做兼容烁挟。
** 關(guān)于65535的問題 請(qǐng)參考由Android 65K方法數(shù)限制引發(fā)的思考**
關(guān)于這個(gè)問題可以采用分包的方案解決婴洼,簡(jiǎn)單的說,分包就是在打包時(shí)將應(yīng)用的代碼分成多個(gè) dex撼嗓,使得主 dex 的方法數(shù)和所需的 LinearAlloc 不超過系統(tǒng)限制柬采。在應(yīng)用啟動(dòng)或運(yùn)行過程中,首先是主 dex 啟動(dòng)運(yùn)行后且警,再加載從 dex粉捻,這樣就繞開了這兩個(gè)限制。但是方案就要解決兩個(gè)問題:一是如何對(duì) dex 進(jìn)行拆分斑芜,二是如何加載從 dex肩刃。
目前的分包方案有Google官方方案和DEX 自動(dòng)拆包和動(dòng)態(tài)加載方案
Google 官方方案
Android官方MultiDex方案使用比較簡(jiǎn)單:
http://developer.android.com/intl/zh-cn/tools/building/multidex.htm
在gradle中添加MultiDex支持
加載classes2.dex
AndroidManifest.xml的application中添加MultiDexApplication,或者如果已經(jīng)重載了Application杏头,則在attachBaseContext()中執(zhí)行MultiDex.install()即可盈包。
MultiDex自動(dòng)拆包帶來的問題:
- 在冷啟動(dòng)時(shí)因?yàn)樾枰惭bDEX文件,如果DEX文件過大時(shí)醇王,處理時(shí)間過長(zhǎng)呢燥,很容易引發(fā)ANR(Application Not Responding);
- 采用MultiDex方案的應(yīng)用可能不能在低于Android 4.0 (API level 14) 機(jī)器上啟動(dòng)寓娩,這個(gè)主要是因?yàn)镈alvik linearAlloc的一個(gè)bug ;
- 采用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è)限制
第一個(gè)坑:?jiǎn)?dòng)時(shí)間過長(zhǎng)
在解決這些坑之前寞埠,先來簡(jiǎn)要看看App啟動(dòng)流程
不難發(fā)現(xiàn),Application.attachBaseContext是我們能控制的最早執(zhí)行的代碼排嫌,在這個(gè)方法里面執(zhí)行MultiDex.install()無疑是最佳時(shí)機(jī)畸裳。
還有一點(diǎn)我們需要了解,首次啟動(dòng)時(shí)Dalvik虛擬機(jī)會(huì)對(duì)classes.dex執(zhí)行dexopt操作淳地,生成ODEX文件怖糊,這個(gè)過程非常耗時(shí)帅容,而執(zhí)行MultiDex.install()必然會(huì)再次對(duì)classes2.dex執(zhí)行dexopt等操作,所有這些操作必須在5秒內(nèi)完成伍伤,否則就ANR并徘;
非首次啟動(dòng)則直接從cache中讀取已經(jīng)執(zhí)行過dexopt的ODEX文件,這個(gè)過程對(duì)啟動(dòng)并無太大影響扰魂÷笃颍基于此,對(duì)attachBaseContext稍作改動(dòng):
首次啟動(dòng)開啟一個(gè)線程來加載classes2.dex劝评,防止阻塞UI線程姐直,非首次啟動(dòng)則同步執(zhí)行。
initAfterDex2Installed()方法是根據(jù)Classes2.dex中結(jié)果蒋畜,將涉及到的相關(guān)初始化工作移到classes2.dex加載完之后執(zhí)行声畏,避免啟動(dòng)問題。
建議在classes2.dex加載完成前姻成,設(shè)置一個(gè)啟動(dòng)等待界面插龄,之后再進(jìn)入主界面,確保用戶體驗(yàn)科展。
第二個(gè)坑:ANR/Crash
實(shí)際上所有這些都是同一個(gè)問題導(dǎo)致的:classes2.dex沒加載完成之前均牢,程序調(diào)用了classes2.dex中的類或者方法!adb logcat看下才睹,基本也就是3類問題:
那么具體如何實(shí)現(xiàn)呢徘跪?還得先簡(jiǎn)單了解下MultiDex編譯過程。
要想完全了解MultiDex編譯過程砂竖,需要對(duì)gradle, groovy有些了解真椿,限于篇幅這里不對(duì)它們作過多介紹,只介紹MultiDex編譯過程中關(guān)鍵的幾個(gè)gradle task乎澄。
task,顧名思義就是任務(wù)的意思测摔,是gradle build的基本單位置济,一個(gè)project所有的build最終是由一個(gè)個(gè)task來完成,以下面一段簡(jiǎn)單的build日志為例:
日志中锋八,generateDebugSources浙于、processDebugJavaRes…都是build過程中依次執(zhí)行的task任務(wù),將上面的Debug替換為Release即為Release build時(shí)的task挟纱,這個(gè)好理解羞酗,下面主要介紹Debug的task。
這些task分別完成不同的功能紊服,最終完成整個(gè)build檀轨,其中與MultiDex編譯過程相關(guān)的task主要有3個(gè):
- collectDebugMultiDexComponents
先收集胸竞,這個(gè)task掃描AndroidManifest.xml中的application、activity参萄、receiver卫枝、provider、service等相關(guān)類讹挎,并將這些類的信息寫入到manifest_keep.txt文件中校赤,該文件位于build/intermediates/multi-dex/debug目錄下。 - shrinkDebugMultiDexComponents
再壓縮筒溃,這個(gè)task會(huì)根據(jù)proguard規(guī)則以及manifest_keep.txt文件來進(jìn)一步優(yōu)化manifest_keep.txt马篮,將其中沒有用到的類刪除,最終生成componentClasses.jar文件怜奖,該文件同樣位于build/intermediates/multi-dex/debug目錄下积蔚。 - createDebugMainDexClassList
最后創(chuàng)建,這個(gè)task會(huì)根據(jù)上步中生成的componentClasses.jar文件中的類烦周,遞歸掃描這些類所有相關(guān)的依賴類尽爆,最終形成maindexlist.txt文件,該文件也位于build/intermediates/multi-dex/debug目錄下读慎,這個(gè)文件中的類最終會(huì)打包進(jìn)classes.dex中漱贱。
需要注意的是,maindexlist.txt文件并沒有完全列出有所的依賴類夭委,如果發(fā)現(xiàn)要查找的那個(gè)class不在maindexlist中幅狮,也無需奇怪。如果一定要確保某個(gè)類分到主dex中株灸,將該類的完整路徑加入到maindexlist中即可崇摄,同時(shí)注意兩點(diǎn):
如果加入的類并不在project中,則gradle構(gòu)建會(huì)忽略這個(gè)類慌烧,
如果加入了多個(gè)相同的類逐抑,則只取其中一個(gè)。
以上3個(gè)task在build日志中都能找到
ANR/Crash如何解決屹蚊?
只需將該類完整路徑添加到maindexlist.txt中即可厕氨!createDebugMainDexClassList這個(gè)task正是實(shí)現(xiàn)這個(gè)操作的關(guān)鍵,主要代碼如下:
這里將需要強(qiáng)制分到classes.dex中的類放在keepin_maindexlist_debug.txt汹粤,這種實(shí)現(xiàn)方式基本能夠解決眼前問題命斧;(此方法在實(shí)踐中并未生效)
另一種方法
新建文件multiDexKeep.pro和multiDexKeep.txt,兩個(gè)文件中加入你要打到mainexlist.txt文件中的類名
.pro文件寫法與混淆配置文件中保護(hù)類的寫法一致;
.txt文件中包路徑+類名.class;
然后嘱兼,在build.gradle中加入:
multiDexKeepProguard file('multiDexKeep.pro')// keep specific classes using proguard syntax multiDexKeepFile file('multiDexKeep.txt')// keep specific classes
最后国葬,rebuild你的工程,重新構(gòu)建完成你就可以在maindexlist.txt文件中找到響應(yīng)的類;
但是這樣還是有問題汇四,主要問題是不可控接奈,任何一次對(duì)代碼的改動(dòng)都有可能導(dǎo)致不同的分包結(jié)果,這就可能隱藏著不同的類導(dǎo)致首次啟動(dòng)失敗船殉,大量測(cè)試結(jié)果也證明了這種方法的不可控性鲫趁。作為開發(fā),代碼不可控?zé)o疑無法忍受利虫,如何改進(jìn)這種方法使得MultiDex可控呢挨厚?
MultiDex的一種改進(jìn)實(shí)現(xiàn)
找出啟動(dòng)過程中所有類及依賴類,強(qiáng)制放入classes.dex中糠惫!
這么做要求啟動(dòng)相關(guān)的類不能太多(實(shí)際上大部分App從啟動(dòng)Application到進(jìn)入MainActivity也就幾個(gè)相關(guān)類)疫剃,同時(shí)盡量讓主界面和二級(jí)界面充分解耦。
如果不想對(duì)現(xiàn)有代碼做太多改動(dòng)硼讽,可以用反射方式調(diào)用二級(jí)界面中的Activity(反射可以避免依賴)巢价,不過調(diào)用時(shí)得要先判斷classes2.dex是否加載完,以防某些二級(jí)界面相關(guān)代碼在classes2.dex中而引起Crash固阁,這么做雖然對(duì)功能實(shí)現(xiàn)并無影響壤躲,但可能導(dǎo)致代碼可維護(hù)性降低。
另外备燃,我們可以控制哪些類在classes.dex中碉克,但無法控制哪些類分到classes2.dex中,以反射方式調(diào)用二級(jí)界面activity可以增大二級(jí)界面相關(guān)類分到classes2.dex中的概率并齐。
尋找啟動(dòng)類
如何找出App啟動(dòng)到主界面顯示這個(gè)過程中的所有類漏麦?
網(wǎng)上能夠找得到的方法比較少,美團(tuán)有自己的腳本程序找啟動(dòng)依賴類况褪,但人家沒開撕贞!源!2舛狻D笈颉!還好Google到了CDA(Class Dependency Analyzer)赐纱,通過這個(gè)工具脊奋,基本能找到啟動(dòng)過程中所有Activity、Application等相關(guān)依賴類疙描,通常會(huì)有一定偏差(會(huì)將某些系統(tǒng)方法也找出來了)。
這時(shí)還需結(jié)合App的所有類來作進(jìn)一步優(yōu)化(獲取App所有類只需反編譯dex文件形成jar讶隐,解壓jar包起胰,再用shell相關(guān)工具處理即可得到),取兩者的交集基本就能找出所有啟動(dòng)依賴類了。這里有一點(diǎn)需注意:必須以debug版本的App來分析效五,下面會(huì)講到為什么地消。
Release版本尋找啟動(dòng)類
為什么要將Release版本單獨(dú)拿出來說呢?
對(duì)畏妖,就是因?yàn)榛煜?br> 混淆可能會(huì)導(dǎo)致每次編譯形成的class文件名不同脉执,代碼的增加或減少也會(huì)對(duì)混淆結(jié)果產(chǎn)生影響,這可能導(dǎo)致每次編譯所需的啟動(dòng)類名都不一樣戒劫,而Debug版本往往不會(huì)做代碼混淆半夷,因此啟動(dòng)過程中的類名基本變化不大。
那么問題來了迅细,如何確定Release版本啟動(dòng)依賴類呢巫橄?
build日志!茵典!
通過build日志湘换,我們發(fā)現(xiàn),proguardRelease這個(gè)task在createReleaseMainDexClassList這個(gè)task之前執(zhí)行统阿,這意味著彩倚,在形成maindexlist之前,我們能夠確切的知道哪些類進(jìn)行了混淆以及混淆之后的類名扶平!如何獲知帆离?proguard的產(chǎn)物給出了答案,build/outputs/mapping/release/目錄下的4個(gè)txt文件就是proguard的產(chǎn)物:
這里mapping.txt文件正是我們需要的蜻直。我們簡(jiǎn)單了解下mapping.txt中文本的結(jié)構(gòu):
從上述信息中盯质,我們知道經(jīng)過代碼混淆,android.support.ActivityManagerCompat在release版中最終打包為android.support.a類概而,并且對(duì)其中的方法呼巷、屬性也進(jìn)行了混淆。
并且注意到赎瑰,文本中對(duì)類混淆的行以”:”結(jié)尾王悍。
這下問題就有解了: - 根據(jù)startup_keep_list_debug.txt文件中的每一行,在mapping.txt中尋找其是否被混淆餐曼。
- 如果被混淆了压储,則讀取經(jīng)過混淆的類。
- 如果沒有被混淆源譬,則直接獲取該類集惋。
通過以上幾個(gè)步驟,即可形成最終Release版本的啟動(dòng)依賴類踩娘。
至此刮刑,尋找啟動(dòng)類工作基本完成,但不難發(fā)現(xiàn)一個(gè)問題,那就是build release版本是將會(huì)更加耗時(shí)雷绢,因?yàn)橐獜膍apping.txt中查找混淆類泛烙,涉及兩層循環(huán),mapping.txt文件通常有上萬行翘紊,這也是這種方法最大的缺陷之一蔽氨。
構(gòu)建得到APK之后,點(diǎn)擊icon帆疟,貌似一切正常work鹉究!
但仍然可能會(huì)遺留一些問題!
通過以上方法找到的啟動(dòng)依賴類并非100%正確鸯匹,幾千上萬個(gè)類中遺漏幾個(gè)畢竟不是小概率事件坊饶,解決方法還是得多次啟動(dòng),通過adb logcat獲取啟動(dòng)日志殴蓬,在日志中查找NoClassDefFoundError匿级、Could not find class、Could not find method等warning染厅。
有必要的話仍需將這些形成warning的類添加到startup_keep_list_debug.txt文件中痘绎,多次啟動(dòng),直到?jīng)]有相關(guān)的warning肖粮,這么做是為了減小未知風(fēng)險(xiǎn)孤页。
至此,這種MultiDex實(shí)現(xiàn)方法基本也就完成了涩馆,后續(xù)會(huì)尋求其他更好的解決方案行施,比如動(dòng)態(tài)加載dex方式等等
性能影響
Dex 分包后,如果是啟動(dòng)時(shí)同步加載魂那,對(duì)應(yīng)用的啟動(dòng)速度會(huì)有一定的影響蛾号,但是主要影響的是安裝后首次啟動(dòng)。這是因?yàn)榘惭b后首次啟動(dòng)時(shí)涯雅,Android 系統(tǒng)會(huì)對(duì)加載的從 dex 做 Dexopt 并生成 ODEX鲜结,而 Dexopt 是比較耗時(shí)的操作,所以對(duì)安裝后首次啟動(dòng)速度影響較大活逆。在非安裝后首次啟動(dòng)時(shí)精刷,應(yīng)用只需加載 ODEX,這個(gè)過程速度很快蔗候,對(duì)啟動(dòng)速度影響不大怒允。同時(shí),從 dex 的大小也直接影響啟動(dòng)速度锈遥,即從dex 越小則啟動(dòng)越快误算。
查閱資料中看仰美,dex 的原始大小在 1M 左右迷殿,經(jīng)過測(cè)試儿礼,安裝后首次啟動(dòng)時(shí),在 GT-I8160(Android 2.3) 上加載耗時(shí)大約 1200ms庆寺,在 N i9250(Android 4.3) 上加載耗時(shí)大約 1000ms蚊夫;非安裝后首次啟動(dòng)時(shí),在這兩臺(tái)測(cè)試手機(jī)上的加載速度分別為約 10ms 和 4ms懦尝。
目前鳳凰金融app知纷,分成兩個(gè)dex,
主dex 7.8m陵霉,從dex 大約108kb琅轧,目前內(nèi)存消耗問題不大;
另一種解決辦法:
專門解決此問題的第三方庫 TurboDex
https://github.com/asLody/TurboDex
總結(jié):
目前鳳凰金融Android端 解決方法數(shù)超過65535 踊挠,考慮到時(shí)間乍桂,人力成本,可以采用官方方法效床,而且測(cè)試 低端機(jī)型酷派 4.3系統(tǒng)時(shí)并未發(fā)現(xiàn)問題睹酌。
后期繼續(xù)對(duì)動(dòng)態(tài)分包進(jìn)行調(diào)研