dex分包的最終方案

首發(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分包.png

這就是所謂的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分包方案。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容