首發(fā)于公眾號: DSGtalk1989
寫在最最前頭
首先你需要知道dex分包到底是啥,早期的android基本不會出現(xiàn)65535的問題,
遙想當年,用eclipse開發(fā)項目的時候,每次出現(xiàn)了65535的問題,通常都會第一時間,
將support-v4和v7的包解壓,然后將沒有使用到的方法刪除,并且重新打包使用,
以此來規(guī)避方法數(shù)超過上限的問題,當然規(guī)避的方法還有很多,包括頻繁的跑lint等。
直到后期官方出來的分包方案為止。
具體我們一般所知的分包均是用來解決項目龐大帶來的方法數(shù)超過65535的的解決方法,盡管現(xiàn)在已經(jīng)擴大了Dexopt的方法數(shù),但是我們依然需要去做低版本的兼容,并且我們也習慣性的在gradle文件中打開dexopt的配置,所以可以看一下你現(xiàn)在所打包的apk(在Android studio打開),會有諸如如下的文件存在

這就是所謂的dex分包了。
需求的由來
實際上IDE默認為我們創(chuàng)建的dex分包方案已經(jīng)足夠我們在大部分的使用情景中不會出現(xiàn)任何問題了。但是如果出現(xiàn)了很特別的錯誤,你就會意識到可能系統(tǒng)的默認分包方案在某些情景下顯得不那么好用了。
因為有同學跟我反映說在使用Tinker(微信開源熱更新)的過程遇到的涉及分包的問題,我們一起來看一下:
java.lang.VerifyError:
Rejecting class io.reactivex.internal.operators.observable.ObservableZip
because it failed compile-time verification (declaration of
'io.reactivex.internal.operators.observable.ObservableZip'
appears in /data/user/0/com.mymoney/tinker/patch-125e62ff/dex/classes3.dex.jar)
這種問題如果你不使用熱更新很難遇到,這是Square公司的Rxjava做的一件很操蛋的事情,這種VerifyError一般會產(chǎn)生在什么情況下呢?
各位可以跟著做如下的步驟試一下:
S1. 創(chuàng)建一個接口類ITestClass
S2. 創(chuàng)建一個類實現(xiàn)這個接口,如TestClass implements ITestClass
S3. 創(chuàng)建一個類,里面增加一個這樣的方法:
void badMethod() {
ITestClass[] arr = new TestClass[3];
if (隨便搞個條件) {
arr = new ITestClass[3];
}
arr[0] = new TestClass();
}
然后把badMethod所在的類和TestClass分到同一個dex,寫個demo加載這個dex,再嘗試調(diào)用badMethod,就會出錯了。
具體的解釋如下,可能會讓你惡心,眩暈,如果介意可直接跳過看結(jié)果
本質(zhì)上這是因為art在校驗aput指令的時候會去確認指向目標數(shù)組的寄存器的類型,如果因為分支語句導致有多種可能,則要求每個可能的類型都要能被resolve。由于補丁dex是分開進行dex2oat的,導致dex2oat在編譯這個dex的時候找不到ITestClass,也就沒法resolve,因此使badMethod所在的類被打上verifyerror標志。運行時一旦嘗試加載有verifyerror標志的類,就會crash。
而直接安裝完整的apk沒問題,是因為apk里面所有符合classN命名的dex是一起做dex2oat的,這樣就不會有某些類resolve不到的問題了。
講簡單一點就是說,如果你在類中出現(xiàn)的if else或者其他分支情況會影響變量的類型走向,就必須要將所有可能的類型都放在同一個dex文件中,否則就會出現(xiàn)VerifyError。
那么再回到上面的error,Rxjava一定是做了如上所述的勾當,導致出現(xiàn)了這個問題,而且正如前面所說,直接安裝完整的apk不會出現(xiàn)這個問題,但是一旦你是補丁安裝,就會秒跪。
到此為止,自定義分包方案的需求就顯得極為強烈了。
因為系統(tǒng)默認的分包方案很有可能會把你的Rxjava拆分到不同的dex文件中,而且你也不能確保之后不會再出現(xiàn)其他的存在VerifyError隱患的場景,當然如果你的補丁方案沒有出現(xiàn)過此類問題話,可以command(ctrl) +w了
從dexknife到dexknife-plus
前者還算比較大眾,后者的話知道的人會更少一點,我們一一來看一下。
-
dexknife
-
眾志成城
https://github.com/ceabie/DexKnifePlugin
屬于dex自定義分包的先行者吧,github的star數(shù)為269(2017-08-05)
具體的繼承方法,直接見項目ReadMe,這里來著重講一講解決的問題和沒有解決的問題。本以為出現(xiàn)了dexknife已經(jīng)可以解決一切了,我們將
io.reactivex包下的所有的文件均放至主dex中,從而來達到規(guī)避VerifyError的效果(注意dexknife能做到的就是規(guī)定什么可以放到主dex中而什么不能放到主dex中)于是我們很快樂的把打好包的apk文件交到測試的手中,以為一勞永逸了,端起桌角的咖啡,品出自己的飄逸與驕傲。
然而帥不過3秒的,測試拿著啟動閃退的android 4.0手機往你臉上一甩,甚至連咖啡的余香都還沒有被完全退散。
插上數(shù)據(jù)線,打開控制臺,系統(tǒng)告訴你說在主dex包中無法找到application文件。OK,這里再次插入科普說明:
android5.0之后不管你再怎么放肆的修改dex文件,系統(tǒng)都會幫你擦屁股,幫你找遍所有的dex文件,直到找到你的application為止。
但是5.0以下則不然,系統(tǒng)一旦發(fā)現(xiàn)你的主dex中沒有application文件,就會直接爆炸。
如果你覺得難以理解,可以去看一看我的ART和Dalvik的區(qū)別,所謂jit和aot,應該可以很好的詮釋這個問題。
jit和aot的傳送門那么怎么辦,抬頭看著測試無奈的盯著你的眼神,因為他很懷疑你的改動,需要讓他把所有的功能都回歸一遍,這時候他可能連殺你的心都有。
-
郁悶
我們只好再次回到我們的配置文件
dexknife.txt# 全局過濾, 如果沒設置 -filter-suggest 并不會應用到建議的maindexlist. # 如果你想要某個包路徑在maindex中,則使用 -keep 選項,即使他已經(jīng)在分包的路徑中. # 當前把所有的rx相關(guān)的類都放在主dex包中 -keep io.reactivex.** # 這條配置可以指定這個包下類在第二dex中. # android.support.v?.** # 使用.class后綴,代表單個類. # -keep android.support.v7.app.AppCompatDialogFragment.class # 不包含Android gradle 插件自動生成的miandex列表. -donot-use-suggest # 將 全局過濾配置應用到 建議的maindexlist中, 但 -donot-use-suggest 要關(guān)閉. # -filter-suggest # 不進行dex分包, 直到 dex 的id數(shù)量超過 65536. -auto-maindex # dex 擴展參數(shù), 例如 --set-max-idx-number=50000 # 如果出現(xiàn) DexException: Too many classes in --main-dex-list, main dex capacity exceeded,則需要調(diào)大數(shù)值 -dex-param --set-max-idx-number=50000 # 顯示miandex的日志. -log-mainlist # 如果你只想過濾 建議的maindexlist, 使用 -suggest-split 和 -suggest-keep. # 如果同時啟用 -filter-suggest, 全局過濾會合并到它們中. # -suggest-split **.MainActivity2.class # -suggest-keep android.support.multidex.**開始不斷的加-keep
然后你會發(fā)現(xiàn)是一場無止境的keep,由于你不使用了AS默認的dex分包方案,然后將你自己需要的io.reactivex包下的文件放到了主dex中,導致原本好好的系統(tǒng)默認的該放主dex的文件被你拆散。你會開始走入如下的循環(huán)- -keep .....application,然后發(fā)現(xiàn)application中使用到了類A
- -keep .....A,然后發(fā)現(xiàn)類A中使用到了類B
- -keep .....B, 然后發(fā)現(xiàn)類B中使用到了類C,類D,類E。。。
- 能堅持到這一步我已經(jīng)覺得你很牛*了,很有毅力的孩子。
但是你會發(fā)現(xiàn)這樣下去很有可能沒個底,即使湊巧你的項目不是很大,有底了,你也會發(fā)現(xiàn)你在dex配置中寫了一大堆的keep,而且一旦你以后在你的application中寫了個新的東西,或者說在application的依賴樹A,B,C,D,E中任何一個類中寫了個新的東西,你都要再過來重新加一次,這樣的維護成本是難以接受的,只好另辟蹊徑。
-
-
dexknife-plus
-
重見光明
https://github.com/TangXiaoLv/Android-Easy-MultiDex
從dexKnife中得到了啟發(fā),并且star數(shù)都超過了660,應該屬于相對比較完善的dex分包方案了,最起碼解決了我們手頭的問題。
我們直接可以從項目的readMe中得到,該方案最成功的地方在于直接分析出了主dex的依賴所在gradle的任務,由此直接解決了龐大的依賴keep,大大釋放了我們的生產(chǎn)力。 -
最終實現(xiàn)
在此無需多言,直接根據(jù)項目中的配置修改成本地化
相比較之前的而言,不同的就是如下幾行配置:#-----------主Dex中必要依賴的腳本配置-----------(支持依賴檢測) #默認保留四大組件中Service,ContentProvider,BroadcastReceiver三大組件,Activity組件選擇性保留,若為空不保留任何Activity #-----------附加類-----------(不支持依賴檢測) # 如果你想要某個包路徑在maindex中,則使用 -keep 選項,即使他已經(jīng)在分包的路徑中.若為空,不保留任意類 -keep io.reactivex.** #將全部類移出主Dex -split **.**如果我們不是特地想要將某個四大組件和它的依賴放入dex,我們就直接可以在第一模塊空著不寫,因為application以及其相關(guān)所有依賴都會默認放入到主dex包下,然后我們可以再將我們需要的包比如
io.reactivex.**放入到主dex下,記得要將之前的主dex包中的類完全移除。
-
總結(jié)
最終,我們從DexKnife過渡到DexKnife-plus,找到最為合適的dex分包方案,解決了自定義dex分包之苦,從此再也不用擔心由于處在不同dex包所帶來的任何問題。只要你想,你可以將任何你所需要的文件放到主dex包中。
同時也帶來依然值得優(yōu)化的空間,比如指定文件放在指定dex包,dex包的優(yōu)化壓縮。大家可以fork DexKnife或者DexKnife-plus,同作者一道努力,創(chuàng)造出更加強大的dex分包方案。