一、背景
隨著業(yè)務規(guī)模發(fā)展,不斷的加入新的功能,添加新的類庫,app的方法數(shù)已經(jīng)超過65535,這樣的情況下就會遇到以下這個錯誤
導致app無法安裝,開發(fā)無法進行。
具體的原因是在早期的 Android 系統(tǒng)中,DexOpt 有兩個問題。
- DexOpt 會把每一個類的方法 id 檢索起來,存在一個鏈表結(jié)構(gòu)里面,但是這個鏈表的長度是用一個 short 類型來保存的,導致了方法 id 的數(shù)目不能夠超過65536個。當一個項目足夠大的時候,顯然這個方法數(shù)的上限是不夠的。
- Dexopt 使用 LinearAlloc 來存儲應用的方法信息。Dalvik LinearAlloc 是一個固定大小的緩沖區(qū)。在Android 版本的歷史上,LinearAlloc 分別經(jīng)歷了4M/5M/8M/16M限制。Android 2.2和2.3的緩沖區(qū)只有5MB,Android 4.x提高到了8MB 或16MB。當方法數(shù)量過多導致超出緩沖區(qū)大小時,也會造成dexopt崩潰。
盡管在新版本的 Android 系統(tǒng)中,DexOpt 修復了方法數(shù)65K的限制問題,并且擴大了 LinearAlloc 限制,但是我們?nèi)匀恍枰獙Φ桶姹镜?Android 系統(tǒng)做兼容。
** 關于65535的問題 請參考由Android 65K方法數(shù)限制引發(fā)的思考**
關于這個問題可以采用分包的方案解決,簡單的說,分包就是在打包時將應用的代碼分成多個 dex,使得主 dex 的方法數(shù)和所需的 LinearAlloc 不超過系統(tǒng)限制。在應用啟動或運行過程中,首先是主 dex 啟動運行后,再加載從 dex,這樣就繞開了這兩個限制。但是方案就要解決兩個問題:一是如何對 dex 進行拆分,二是如何加載從 dex。
目前的分包方案有Google官方方案和DEX 自動拆包和動態(tài)加載方案
Google 官方方案
Android官方MultiDex方案使用比較簡單:
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自動拆包帶來的問題:
- 在冷啟動時因為需要安裝DEX文件,如果DEX文件過大時,處理時間過長,很容易引發(fā)ANR(Application Not Responding);
- 采用MultiDex方案的應用可能不能在低于Android 4.0 (API level 14) 機器上啟動,這個主要是因為Dalvik linearAlloc的一個bug ;
- 采用MultiDex方案的應用因為需要申請一個很大的內(nèi)存,在運行時可能導致程序的崩潰,這個主要是因為Dalvik linearAlloc 的一個限制,這個限制在 Android 4.0 (API level 14)已經(jīng)增加了, 應用也有可能在低于 Android 5.0 (API level 21)版本的機器上觸發(fā)這個限制
第一個坑:啟動時間過長
在解決這些坑之前,先來簡要看看App啟動流程
不難發(fā)現(xiàn),Application.attachBaseContext是我們能控制的最早執(zhí)行的代碼,在這個方法里面執(zhí)行MultiDex.install()無疑是最佳時機。
還有一點我們需要了解,首次啟動時Dalvik虛擬機會對classes.dex執(zhí)行dexopt操作,生成ODEX文件,這個過程非常耗時,而執(zhí)行MultiDex.install()必然會再次對classes2.dex執(zhí)行dexopt等操作,所有這些操作必須在5秒內(nèi)完成,否則就ANR;
非首次啟動則直接從cache中讀取已經(jīng)執(zhí)行過dexopt的ODEX文件,這個過程對啟動并無太大影響?;诖耍瑢ttachBaseContext稍作改動:
首次啟動開啟一個線程來加載classes2.dex,防止阻塞UI線程,非首次啟動則同步執(zhí)行。
initAfterDex2Installed()方法是根據(jù)Classes2.dex中結(jié)果,將涉及到的相關初始化工作移到classes2.dex加載完之后執(zhí)行,避免啟動問題。
建議在classes2.dex加載完成前,設置一個啟動等待界面,之后再進入主界面,確保用戶體驗。
第二個坑:ANR/Crash
實際上所有這些都是同一個問題導致的:classes2.dex沒加載完成之前,程序調(diào)用了classes2.dex中的類或者方法!adb logcat看下,基本也就是3類問題:
那么具體如何實現(xiàn)呢?還得先簡單了解下MultiDex編譯過程。
要想完全了解MultiDex編譯過程,需要對gradle, groovy有些了解,限于篇幅這里不對它們作過多介紹,只介紹MultiDex編譯過程中關鍵的幾個gradle task。
task,顧名思義就是任務的意思,是gradle build的基本單位,一個project所有的build最終是由一個個task來完成,以下面一段簡單的build日志為例:
日志中,generateDebugSources、processDebugJavaRes…都是build過程中依次執(zhí)行的task任務,將上面的Debug替換為Release即為Release build時的task,這個好理解,下面主要介紹Debug的task。
這些task分別完成不同的功能,最終完成整個build,其中與MultiDex編譯過程相關的task主要有3個:
- collectDebugMultiDexComponents
先收集,這個task掃描AndroidManifest.xml中的application、activity、receiver、provider、service等相關類,并將這些類的信息寫入到manifest_keep.txt文件中,該文件位于build/intermediates/multi-dex/debug目錄下。 - shrinkDebugMultiDexComponents
再壓縮,這個task會根據(jù)proguard規(guī)則以及manifest_keep.txt文件來進一步優(yōu)化manifest_keep.txt,將其中沒有用到的類刪除,最終生成componentClasses.jar文件,該文件同樣位于build/intermediates/multi-dex/debug目錄下。 - createDebugMainDexClassList
最后創(chuàng)建,這個task會根據(jù)上步中生成的componentClasses.jar文件中的類,遞歸掃描這些類所有相關的依賴類,最終形成maindexlist.txt文件,該文件也位于build/intermediates/multi-dex/debug目錄下,這個文件中的類最終會打包進classes.dex中。
需要注意的是,maindexlist.txt文件并沒有完全列出有所的依賴類,如果發(fā)現(xiàn)要查找的那個class不在maindexlist中,也無需奇怪。如果一定要確保某個類分到主dex中,將該類的完整路徑加入到maindexlist中即可,同時注意兩點:
如果加入的類并不在project中,則gradle構(gòu)建會忽略這個類,
如果加入了多個相同的類,則只取其中一個。
以上3個task在build日志中都能找到
ANR/Crash如何解決?
只需將該類完整路徑添加到maindexlist.txt中即可!createDebugMainDexClassList這個task正是實現(xiàn)這個操作的關鍵,主要代碼如下:
這里將需要強制分到classes.dex中的類放在keepin_maindexlist_debug.txt,這種實現(xiàn)方式基本能夠解決眼前問題;(此方法在實踐中并未生效)
另一種方法
新建文件multiDexKeep.pro和multiDexKeep.txt,兩個文件中加入你要打到mainexlist.txt文件中的類名
.pro文件寫法與混淆配置文件中保護類的寫法一致;
.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文件中找到響應的類;
但是這樣還是有問題,主要問題是不可控,任何一次對代碼的改動都有可能導致不同的分包結(jié)果,這就可能隱藏著不同的類導致首次啟動失敗,大量測試結(jié)果也證明了這種方法的不可控性。作為開發(fā),代碼不可控無疑無法忍受,如何改進這種方法使得MultiDex可控呢?
MultiDex的一種改進實現(xiàn)
找出啟動過程中所有類及依賴類,強制放入classes.dex中!
這么做要求啟動相關的類不能太多(實際上大部分App從啟動Application到進入MainActivity也就幾個相關類),同時盡量讓主界面和二級界面充分解耦。
如果不想對現(xiàn)有代碼做太多改動,可以用反射方式調(diào)用二級界面中的Activity(反射可以避免依賴),不過調(diào)用時得要先判斷classes2.dex是否加載完,以防某些二級界面相關代碼在classes2.dex中而引起Crash,這么做雖然對功能實現(xiàn)并無影響,但可能導致代碼可維護性降低。
另外,我們可以控制哪些類在classes.dex中,但無法控制哪些類分到classes2.dex中,以反射方式調(diào)用二級界面activity可以增大二級界面相關類分到classes2.dex中的概率。
尋找啟動類
如何找出App啟動到主界面顯示這個過程中的所有類?
網(wǎng)上能夠找得到的方法比較少,美團有自己的腳本程序找啟動依賴類,但人家沒開!源?。。。∵€好Google到了CDA(Class Dependency Analyzer),通過這個工具,基本能找到啟動過程中所有Activity、Application等相關依賴類,通常會有一定偏差(會將某些系統(tǒng)方法也找出來了)。
這時還需結(jié)合App的所有類來作進一步優(yōu)化(獲取App所有類只需反編譯dex文件形成jar,解壓jar包,再用shell相關工具處理即可得到),取兩者的交集基本就能找出所有啟動依賴類了。這里有一點需注意:必須以debug版本的App來分析,下面會講到為什么。
Release版本尋找啟動類
為什么要將Release版本單獨拿出來說呢?
對,就是因為混淆!
混淆可能會導致每次編譯形成的class文件名不同,代碼的增加或減少也會對混淆結(jié)果產(chǎn)生影響,這可能導致每次編譯所需的啟動類名都不一樣,而Debug版本往往不會做代碼混淆,因此啟動過程中的類名基本變化不大。
那么問題來了,如何確定Release版本啟動依賴類呢?
build日志?。?br> 通過build日志,我們發(fā)現(xiàn),proguardRelease這個task在createReleaseMainDexClassList這個task之前執(zhí)行,這意味著,在形成maindexlist之前,我們能夠確切的知道哪些類進行了混淆以及混淆之后的類名!如何獲知?proguard的產(chǎn)物給出了答案,build/outputs/mapping/release/目錄下的4個txt文件就是proguard的產(chǎn)物:
這里mapping.txt文件正是我們需要的。我們簡單了解下mapping.txt中文本的結(jié)構(gòu):
從上述信息中,我們知道經(jīng)過代碼混淆,android.support.ActivityManagerCompat在release版中最終打包為android.support.a類,并且對其中的方法、屬性也進行了混淆。
并且注意到,文本中對類混淆的行以”:”結(jié)尾。
這下問題就有解了: - 根據(jù)startup_keep_list_debug.txt文件中的每一行,在mapping.txt中尋找其是否被混淆。
- 如果被混淆了,則讀取經(jīng)過混淆的類。
- 如果沒有被混淆,則直接獲取該類。
通過以上幾個步驟,即可形成最終Release版本的啟動依賴類。
至此,尋找啟動類工作基本完成,但不難發(fā)現(xiàn)一個問題,那就是build release版本是將會更加耗時,因為要從mapping.txt中查找混淆類,涉及兩層循環(huán),mapping.txt文件通常有上萬行,這也是這種方法最大的缺陷之一。
構(gòu)建得到APK之后,點擊icon,貌似一切正常work!
但仍然可能會遺留一些問題!
通過以上方法找到的啟動依賴類并非100%正確,幾千上萬個類中遺漏幾個畢竟不是小概率事件,解決方法還是得多次啟動,通過adb logcat獲取啟動日志,在日志中查找NoClassDefFoundError、Could not find class、Could not find method等warning。
有必要的話仍需將這些形成warning的類添加到startup_keep_list_debug.txt文件中,多次啟動,直到?jīng)]有相關的warning,這么做是為了減小未知風險。
至此,這種MultiDex實現(xiàn)方法基本也就完成了,后續(xù)會尋求其他更好的解決方案,比如動態(tài)加載dex方式等等
性能影響
Dex 分包后,如果是啟動時同步加載,對應用的啟動速度會有一定的影響,但是主要影響的是安裝后首次啟動。這是因為安裝后首次啟動時,Android 系統(tǒng)會對加載的從 dex 做 Dexopt 并生成 ODEX,而 Dexopt 是比較耗時的操作,所以對安裝后首次啟動速度影響較大。在非安裝后首次啟動時,應用只需加載 ODEX,這個過程速度很快,對啟動速度影響不大。同時,從 dex 的大小也直接影響啟動速度,即從dex 越小則啟動越快。
查閱資料中看,dex 的原始大小在 1M 左右,經(jīng)過測試,安裝后首次啟動時,在 GT-I8160(Android 2.3) 上加載耗時大約 1200ms,在 N i9250(Android 4.3) 上加載耗時大約 1000ms;非安裝后首次啟動時,在這兩臺測試手機上的加載速度分別為約 10ms 和 4ms。
目前鳳凰金融app,分成兩個dex,
主dex 7.8m,從dex 大約108kb,目前內(nèi)存消耗問題不大;
另一種解決辦法:
專門解決此問題的第三方庫 TurboDex
https://github.com/asLody/TurboDex
總結(jié):
目前鳳凰金融Android端 解決方法數(shù)超過65535 ,考慮到時間,人力成本,可以采用官方方法,而且測試 低端機型酷派 4.3系統(tǒng)時并未發(fā)現(xiàn)問題。
后期繼續(xù)對動態(tài)分包進行調(diào)研