dex分包的最终方案

首发于公众号: DSGtalk1989

写在最最前头

首先你需要知道dex分包到底是啥,早期的android基本不会出现65535的问题,
遥想当年,用eclipse开发项目的时候,每次出现了65535的问题,通常都会第一时间,
将support-v4和v7的包解压,然后将没有使用到的方法删除,并且重新打包使用,
以此来规避方法数超过上限的问题,当然规避的方法还有很多,包括频繁的跑lint等。
直到后期官方出来的分包方案为止。

具体我们一般所知的分包均是用来解决项目庞大带来的方法数超过65535的的解决方法,尽管现在已经扩大了Dexopt的方法数,但是我们依然需要去做低版本的兼容,并且我们也习惯性的在gradle文件中打开dexopt的配置,所以可以看一下你现在所打包的apk(在Android studio打开),会有诸如如下的文件存在

dex分包的最终方案_第1张图片
dex分包.png

这就是所谓的dex分包了。


需求的由来

实际上IDE默认为我们创建的dex分包方案已经足够我们在大部分的使用情景中不会出现任何问题了。但是如果出现了很特别的错误,你就会意识到可能系统的默认分包方案在某些情景下显得不那么好用了。

因为有同学跟我反映说在使用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一般会产生在什么情况下呢?

各位可以跟着做如下的步骤试一下:
S1. 创建一个接口类ITestClass
S2. 创建一个类实现这个接口,如TestClass implements ITestClass
S3. 创建一个类,里面增加一个这样的方法:

void badMethod() {
    ITestClass[] arr = new TestClass[3];
    if (随便搞个条件) {
        arr = new ITestClass[3];
    }
    arr[0] = new TestClass();
}

然后把badMethod所在的类和TestClass分到同一个dex,写个demo加载这个dex,再尝试调用badMethod,就会出错了。

具体的解释如下,可能会让你恶心,眩晕,如果介意可直接跳过看结果

本质上这是因为art在校验aput指令的时候会去确认指向目标数组的寄存器的类型,如果因为分支语句导致有多种可能,则要求每个可能的类型都要能被resolve。由于补丁dex是分开进行dex2oat的,导致dex2oat在编译这个dex的时候找不到ITestClass,也就没法resolve,因此使badMethod所在的类被打上verifyerror标志。运行时一旦尝试加载有verifyerror标志的类,就会crash。
而直接安装完整的apk没问题,是因为apk里面所有符合classN命名的dex是一起做dex2oat的,这样就不会有某些类resolve不到的问题了。

讲简单一点就是说,如果你在类中出现的if else或者其他分支情况会影响变量的类型走向,就必须要将所有可能的类型都放在同一个dex文件中,否则就会出现VerifyError

那么再回到上面的error,Rxjava一定是做了如上所述的勾当,导致出现了这个问题,而且正如前面所说,直接安装完整的apk不会出现这个问题,但是一旦你是补丁安装,就会秒跪。

到此为止,自定义分包方案的需求就显得极为强烈了。
因为系统默认的分包方案很有可能会把你的Rxjava拆分到不同的dex文件中,而且你也不能确保之后不会再出现其他的存在VerifyError隐患的场景,当然如果你的补丁方案没有出现过此类问题话,可以command(ctrl) +w

从dexknife到dexknife-plus

前者还算比较大众,后者的话知道的人会更少一点,我们一一来看一下。

  • dexknife

    • 众志成城

      https://github.com/ceabie/DexKnifePlugin
      属于dex自定义分包的先行者吧,github的star数为269(2017-08-05)
      具体的继承方法,直接见项目ReadMe,这里来着重讲一讲解决的问题和没有解决的问题。

      本以为出现了dexknife已经可以解决一切了,我们将io.reactivex包下的所有的文件均放至主dex中,从而来达到规避VerifyError的效果(注意dexknife能做到的就是规定什么可以放到主dex中而什么不能放到主dex中)

      于是我们很快乐的把打好包的apk文件交到测试的手中,以为一劳永逸了,端起桌角的咖啡,品出自己的飘逸与骄傲。

      然而帅不过3秒的,测试拿着启动闪退的android 4.0手机往你脸上一甩,甚至连咖啡的余香都还没有被完全退散。

      插上数据线,打开控制台,系统告诉你说在主dex包中无法找到application文件。OK,这里再次插入科普说明:

      android5.0之后不管你再怎么放肆的修改dex文件,系统都会帮你擦屁股,帮你找遍所有的dex文件,直到找到你的application为止。
      但是5.0以下则不然,系统一旦发现你的主dex中没有application文件,就会直接爆炸。
      如果你觉得难以理解,可以去看一看我的ART和Dalvik的区别,所谓jit和aot,应该可以很好的诠释这个问题。
      jit和aot的传送门

      那么怎么办,抬头看着测试无奈的盯着你的眼神,因为他很怀疑你的改动,需要让他把所有的功能都回归一遍,这时候他可能连杀你的心都有。

    • 郁闷

      我们只好再次回到我们的配置文件dexknife.txt

       # 全局过滤, 如果没设置 -filter-suggest 并不会应用到建议的maindexlist.
       # 如果你想要某个包路径在maindex中,则使用 -keep 选项,即使他已经在分包的路径中.
      
      
       # 当前把所有的rx相关的类都放在主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 要关闭.
       # -filter-suggest
      
       # 不进行dex分包, 直到 dex 的id数量超过 65536.
       -auto-maindex
      
       # dex 扩展参数, 例如 --set-max-idx-number=50000
       # 如果出现 DexException: Too many classes in --main-dex-list,         main dex capacity exceeded,则需要调大数值
       -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
      然后你会发现是一场无止境的keep,由于你不使用了AS默认的dex分包方案,然后将你自己需要的io.reactivex包下的文件放到了主dex中,导致原本好好的系统默认的该放主dex的文件被你拆散。你会开始走入如下的循环

      • -keep .....application,然后发现application中使用到了类A
      • -keep .....A,然后发现类A中使用到了类B
      • -keep .....B, 然后发现类B中使用到了类C,类D,类E。。。
      • 能坚持到这一步我已经觉得你很牛*了,很有毅力的孩子。

      但是你会发现这样下去很有可能没个底,即使凑巧你的项目不是很大,有底了,你也会发现你在dex配置中写了一大堆的keep,而且一旦你以后在你的application中写了个新的东西,或者说在application的依赖树A,B,C,D,E中任何一个类中写了个新的东西,你都要再过来重新加一次,这样的维护成本是难以接受的,只好另辟蹊径。

  • dexknife-plus

    • 重见光明

      https://github.com/TangXiaoLv/Android-Easy-MultiDex
      从dexKnife中得到了启发,并且star数都超过了660,应该属于相对比较完善的dex分包方案了,最起码解决了我们手头的问题。
      我们直接可以从项目的readMe中得到,该方案最成功的地方在于直接分析出了主dex的依赖所在gradle的任务,由此直接解决了庞大的依赖keep,大大释放了我们的生产力。

    • 最终实现

      在此无需多言,直接根据项目中的配置修改成本地化
      相比较之前的而言,不同的就是如下几行配置:

      #-----------主Dex中必要依赖的脚本配置-----------(支持依赖检测)
      #默认保留四大组件中Service,ContentProvider,BroadcastReceiver三大组件,Activity组件选择性保留,若为空不保留任何Activity
      #-----------附加类-----------(不支持依赖检测)
      # 如果你想要某个包路径在maindex中,则使用 -keep 选项,即使他已经在分包的路径中.若为空,不保留任意类
      -keep io.reactivex.**
      #将全部类移出主Dex
      -split **.**
      

      如果我们不是特地想要将某个四大组件和它的依赖放入dex,我们就直接可以在第一模块空着不写,因为application以及其相关所有依赖都会默认放入到主dex包下,然后我们可以再将我们需要的包比如io.reactivex.**放入到主dex下,记得要将之前的主dex包中的类完全移除。

总结

最终,我们从DexKnife过渡到DexKnife-plus,找到最为合适的dex分包方案,解决了自定义dex分包之苦,从此再也不用担心由于处在不同dex包所带来的任何问题。只要你想,你可以将任何你所需要的文件放到主dex包中。

同时也带来依然值得优化的空间,比如指定文件放在指定dex包,dex包的优化压缩。大家可以fork DexKnife或者DexKnife-plus,同作者一道努力,创造出更加强大的dex分包方案。

你可能感兴趣的:(dex分包的最终方案)