MultiDex使用方法及由此导致的crash、ANR问题解决方案

Android开发的朋友,如果是在开发一款中大型应用时,都会碰到这么一个问题,就是dex分拆问题, google给出的解决方案MultiDex。

现象:

有些APP本身功能比较多,再加上一些其它三方的SDK,慢慢的发现dex越来越大,直到有一天编译出现如下错误:

Error:The number of method references in a .dex file cannot exceed 64K.
Learn how to resolve this issue at https://developer.android.com/tools/building/multidex.html

错误原因比较明确了,打开Gradle Console查看详细信息,我们在一堆错误中找到如下提醒:

"UNEXPECTED TOP-LEVEL EXCEPTION:\ncom.android.dex.DexIndexOverflowException: method ID not in [0, 0xffff]: 65536\n\tat com.android.dx.merge.DexMerger$6.updateIndex(DexMerger

原因:

问题原因是因为早期Android系统设计时用一个short来表示dex里的每个method的id,我们知道short的上限是64K,所以就遗留了这样一个问题,既然存在了,肯定要找方法解决,Google刚开始给出建议是使用proguard,但是再怎么proguard也还是会突破64K的,所以后面Google给出了MultiDex方案。就是我们经常看到的一个apk里,有classes.dex, classes2.dex,甚至还有classes3.dex等。

MultiDex实现步骤:

用这种方法来突破64K的method id数量的限制,具体实现步骤如下:
1. 在Module的build.gradle里添加
multiDexEnabled true

例如:

android {
    compileSdkVersion 22
    buildToolsVersion "22.0.1"

    defaultConfig {
        applicationId "x.x.x"
        minSdkVersion 15
        targetSdkVersion 22
        versionCode 17
        versionName "1.2.9"

        //添加这一行
        multiDexEnabled true

    }

}

2. 接着在Module的build.gradle里添加

dependencies {
  compile 'com.android.support:multidex:1.0.0'
}

3.第三步有两种情况,
1)如果你的apk没有定义application,则在AndroidManifest.xml里的application里做如下修改:
添加MultiDexApplication

"@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme"
        android:name="android.support.multidex.MultiDexApplication"
        tools:replace="android:icon, android:name"
       >

2)如果apk有自己的appliation,比如叫MyApplication,则在MyApplication实现里加如下代码:

 @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        MultiDex.install(this);
    }

至此,MultiDex的编译配置已经完成,你只要重新编译即可,apk就会生成两个(或多个) dex。如果有兴趣了解下MultiDex的实现原理,可以查看源代码,https://android.googlesource.com/platform/frameworks/multidex 源代码相对比较简单的。基本原理是使用java的class loader,这个在一些热修复技术上也在使用,比如企鹅工厂开源的Tinker。

存在的坑及解决方法:

虽然MultiDex分包已经完成,但是实际使用过程中,可能存在一些坑,比如之前笔者碰到的一个VerifyError的错误(http://blog.csdn.net/zhuobattle/article/details/47153025)
1. 分拆导致的crash

这个问题的表现除了报VerifyError外,还有可能报Could not find class,NoClassDefFoundError, Could not find method等。是因为我们在main dex中调用的函数或类被放在了classes2.dex中,而在classes2.dex还没有被完全加载前,调用这些api就会导致这种问题。要确认是否是这个问题导致的错误,我们可以查看:
app\build\intermediates\multi-dex\debug\maindexlist.txt 这个文本文件,这里列出来的类都会被放在主dex中,那么问题是我们要如何解决这个问题呢?
解决方案:
首先,我们来看下在编译过程中,multidex是做了哪几步,这个打开Gradle Console可以找到multidex相关的步骤,其中一个步骤是关键就是生成maindexlist.txt的步骤:createDebugMainDexClassList就是这里生成maindexlist.txt 的,但是这个文件直接修改又没有什么用,因为每次编译都会重新生成一次的,笔者在实践者发现可以用自定义的方式:multiDexKeepFile file(‘multiDexKeep.txt’)
例子:

android {
    compileSdkVersion 22
    buildToolsVersion "22.0.1"


    defaultConfig {
        applicationId "x.x.x"
        minSdkVersion 15
        targetSdkVersion 22
        versionCode 17
        versionName "1.2.9"

        multiDexEnabled true

        //添加这一行
        multiDexKeepFile file('multiDexKeep.txt')
    }

}

内容和上面提到的createDebugMainDexClassList生成的maindexlist.txt一样即可,记得把这个multiDexKeep.txt文件放在app目录 下。multiDexKeep.txt内容可以如下:

com/test/Util.class
com/test/help/b.class

实测起作用,被keep的class全都留在了classes.dex中(release版本要配合mapping使用)。

2. ANR
也就是我们常说的卡顿,为什么会卡顿呢?这里因为我们把multidex的install放在了attachBaseContext中,而这个调用又是在MainActivity的onCreate之前的,所以如果2.dex,3.dex第一次加载时间很长(生成odex文件会耗费一定的时间), 就有可能会导致第一次启动卡上一小会(实测在Dalvik手机上一般classes.dex比较小的情况下,卡顿不明显,线上就不好说了)。

解决方法是在 APP第一次启动时(卸载、重装都会做一遍2odex,具体可以查看/data/data//code_cache/secondary-dexes/目录下的odex文件,所以这里判断要准确),把install放到异步线程里去做。这是很多网上的解决方法,但是如果这样做,你就必须再写一个类似initAfterDex2Installed(),来保证2.dex里的类不会提前被调用到,或者输出一个启动界面,停留几秒的样子,比如很多APP启动都有开机广告,或者开机画面,以此来解决app在2.dex加载之前部分功能无法使用的问题。网上大多是这种解决方案。本人测试后发现,如果MultiDex.install(this),放在后面或者异步来做的话,在MainActivity里的onCreate函数:
setContentView这里就出错了,堆栈如下:

java.lang.NoClassDefFoundError: android.support.v7.appcompat.R$attr
                                                       at android.support.v7.app.AppCompatDelegateImplV7.ensureSubDecor(AppCompatDelegateImplV7.java:289)
                                                       at android.support.v7.app.AppCompatDelegateImplV7.setContentView(AppCompatDelegateImplV7.java:246)
                                                       at android.support.v7.app.AppCompatActivity.setContentView(AppCompatActivity.java:106)
                                                       at com.cn.x.x.MainActivity.onCreate(MainActivity.java:86)

很明显android.support.v7.appcompat.R$attr在classes2.dex中,在调用时,还没有完成classes2.dex的加载,所以如果要解决的话,要不把这个类放到maindex中,要不让MainActivity的onCreate函数延迟调用。如果其它类还有类似问题,就要一个一个的试,成本有点高,有可能在不同系统,不同手机上还会出各种奇芭错误。由此看来,install延期加载方案并不合适。为了说明问题,也分析了网易几款大的产品,比如新闻、云音乐,全部是使用的mutlidex方式,都没有延期加载,全部是在application的attachBaseContext直接调用install.

那么我们要如何解决 2.dex, 3.dex在第一次启动apk时,有可能产生的耗时呢?

1)重新设计,或者在开发之前就设计好,使用插件化的方式来解决maindex的问题,把一些功能做成插件,保证这些dex在首屏启动时不需要被加载
2) 自己实现多dex框架,有兴趣可以看下微信的实现框架,并没有使用MultiDex,而是使用自己的Tinker(一种动态加载dex的方案,也被用于热更新)
微信使用的是TinkerAppliation. 另外一个例子是手机QQ的实现技术,手Q里居然有classes6.dex,也就是总共有6个dex,感兴趣的朋友可以分析下手Q的实现方案,笔者动态看过,基本上也是在手Q启动界面还没出来时,所有的dex会全部完成2odex的转换,在手机上第一次运行还是会花费不少时间的。而且笔者测试了很多手机,基本上2个dex不会发生anr,当然真实情况怎么样,还是要放到线上才能说明问题,也就是接收众多手机,不同品牌,不同性能的手机测试才知道,好在有一些知名公司的质量跟踪平台可以线上捕捉这些ANR,比如网易云捕

3) ART以后的实际情况是,不管你分成几个dex,在安装时都已经全部完成OAT的转换,这样分dex至少在ART以后也就是5.0(部分4.4机型可以选ART模式)以后不存在调用install占用时间的问题,而且实际测试也是在5.0以后,即使不调用install(this)也能正常工作,为了证实,我们拿网易新闻测试,在安装完后,在目录 /data/dalvik-cache//data@[email protected]@[email protected],这个oat文件有84M,所以你可以知道
为什么第一次安装相比 dalivk慢了。
打开这个文件,后缀虽然还是dex,其实是一个ELF文件,也就是OAT文件。
MultiDex使用方法及由此导致的crash、ANR问题解决方案_第1张图片
classes.dex在这里。
MultiDex使用方法及由此导致的crash、ANR问题解决方案_第2张图片

MultiDex使用方法及由此导致的crash、ANR问题解决方案_第3张图片
classes2.dex, classes3.dex都已经被优化成OAT文件了。

这些处理都已经在安装的时候完成,就不存在启动时再对classes2.dex和classes3.dex进行处理了,避免了第一次启动可能导致的ANR.

总结

总结一下就是我们可以通过自定义maindexlist来控制哪些类一定出现在main dex中,这样可以避免crash;
而针对ANR还是不建议使用异步加载,合理设计和插件形式会比较合理,如果大家有其它更好的方法可以讨论。
不过在ART以后,不管是classes.dex还是classes2.dex, classes3.dex在安装时就已经完成了OAT的转换了,由分包导致的ANR的可能性就小了很多。

文章写完的时候,翻了一下其它博客,找到有一位朋友对ANR另外的一种解决方案,可以借鉴:
http://blog.csdn.net/qq_17766199/article/details/51285868

你可能感兴趣的:(Android,MultiDex,android分包)