Android Dex 分包指南

一缰冤、背景

隨著業(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)拆包帶來的問題:

  1. 在冷啟動(dòng)時(shí)因?yàn)樾枰惭bDEX文件,如果DEX文件過大時(shí)醇王,處理時(shí)間過長(zhǎng)呢燥,很容易引發(fā)ANR(Application Not Responding);
  2. 采用MultiDex方案的應(yīng)用可能不能在低于Android 4.0 (API level 14) 機(jī)器上啟動(dòng)寓娩,這個(gè)主要是因?yàn)镈alvik linearAlloc的一個(gè)bug ;
  3. 采用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è):

  1. collectDebugMultiDexComponents
    先收集胸竞,這個(gè)task掃描AndroidManifest.xml中的application、activity参萄、receiver卫枝、provider、service等相關(guān)類讹挎,并將這些類的信息寫入到manifest_keep.txt文件中校赤,該文件位于build/intermediates/multi-dex/debug目錄下。
  2. shrinkDebugMultiDexComponents
    再壓縮筒溃,這個(gè)task會(huì)根據(jù)proguard規(guī)則以及manifest_keep.txt文件來進(jìn)一步優(yōu)化manifest_keep.txt马篮,將其中沒有用到的類刪除,最終生成componentClasses.jar文件怜奖,該文件同樣位于build/intermediates/multi-dex/debug目錄下积蔚。
  3. 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é)尾王悍。
    這下問題就有解了:
  4. 根據(jù)startup_keep_list_debug.txt文件中的每一行,在mapping.txt中尋找其是否被混淆餐曼。
  5. 如果被混淆了压储,則讀取經(jīng)過混淆的類。
  6. 如果沒有被混淆源譬,則直接獲取該類集惋。
    通過以上幾個(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)研
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市剩檀,隨后出現(xiàn)的幾起案子憋沿,更是在濱河造成了極大的恐慌,老刑警劉巖沪猴,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件辐啄,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡运嗜,警方通過查閱死者的電腦和手機(jī)壶辜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來洗出,“玉大人士复,你說我怎么就攤上這事◆婊睿” “怎么了阱洪?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)菠镇。 經(jīng)常有香客問我冗荸,道長(zhǎng),這世上最難降的妖魔是什么利耍? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任蚌本,我火速辦了婚禮盔粹,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘程癌。我一直安慰自己舷嗡,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布嵌莉。 她就那樣靜靜地躺著进萄,像睡著了一般。 火紅的嫁衣襯著肌膚如雪锐峭。 梳的紋絲不亂的頭發(fā)上中鼠,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音沿癞,去河邊找鬼援雇。 笑死,一個(gè)胖子當(dāng)著我的面吹牛椎扬,可吹牛的內(nèi)容都是我干的惫搏。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼盗舰,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼晶府!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起钻趋,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤川陆,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后蛮位,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體较沪,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年失仁,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了尸曼。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡萄焦,死狀恐怖控轿,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情拂封,我是刑警寧澤茬射,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站冒签,受9級(jí)特大地震影響在抛,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜萧恕,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一刚梭、第九天 我趴在偏房一處隱蔽的房頂上張望肠阱。 院中可真熱鬧,春花似錦朴读、人聲如沸屹徘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽缘回。三九已至,卻和暖如春典挑,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背啦吧。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工您觉, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人授滓。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓琳水,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親般堆。 傳聞我的和親對(duì)象是個(gè)殘疾皇子在孝,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容