出现的原因:
Android 5.0 之前版本的 Dalvik 可执行文件分包支持
Android 5.0(API 级别 21)之前的平台版本使用 Dalvik 运行时来执行应用代码。默认情况下,Dalvik 限制应用的每个 APK 只能使用单个 classes.dex
字节码文件。要想绕过这一限制,您可以使用 MultiDex,它会成为您的应用主要 DEX 文件的一部分,然后管理对其他 DEX 文件及其所包含代码的访问。
当Android系统安装一个应用的时候,有一步是对Dex进行优化,这个过程有一个专门的工具来处理,叫DexOpt。DexOpt的执行过程是在第一次加载Dex文件的时候执行的。这个过程会生成一个ODEX文件,即Optimised Dex。执行ODex的效率会比直接执行Dex文件的效率要高很多。
但是在早期的Android系统中,DexOpt有一个问题,DexOpt会把每一个类的方法id检索起来,存在一个链表结构里面。但是这个链表的长度是用一个short类型来保存的,导致了方法id的数目不能够超过65536个。当一个项目足够大的时候,显然这个方法数的上限是不够的。尽管在新版本的Android系统中,DexOpt修复了这个问题,但是我们仍然需要对低版本的Android系统做兼容。
为了解决方法数超限的问题,需要将该dex文件拆成两个或多个,为此谷歌官方推出了multidex兼容包,配合AndroidStudio实现了一个APK包含多个dex的功能。
Android 5.0 及更高版本的 Dalvik 可执行文件分包支持
Android 5.0(API 级别 21)及更高版本使用名为 ART 的运行时,后者原生支持从 APK 文件加载多个 DEX 文件。ART 在应用安装时执行预编译,扫描 classesN.dex
文件,并将它们编译成单个 .oat
文件,供 Android 设备执行。因此,如果您的 minSdkVersion
为 21 或更高值,则不需要 Dalvik 可执行文件分包支持库。
如需了解有关 Android 5.0 运行时的详细信息,请参阅 ART 和 Dalvik。
目前在已经在API 21中提供了通用的解决方案,那就是android-support-multidex.jar. 这个jar包最低可以支持到API 4的版本(Android L及以上版本会默认支持mutidex).
引起的错误:
Android方法数不能超过65K的限制:
Conversion to Dalvik format failed:Unable toexecute dex: method ID not in [0, 0xffff]: 65536
可能有些同学会说,解决这个问题很简单,我们只需要在Project.proterty中配置一句话就Ok啦,
dex.force.jumbo=true
是的,加入了这句话,确实可以让你的应用通过编译,但是在一些2.3系统的机器上很容易出现
INSTALL_FAILED_DEXOPT异常
对于以上两个异常,我们先来分析一下原因:
1、Android系统中,一个Dex文件中存储方法id用的是short类型数据,所以导致你的dex中方法不能超过65k
.Java文件编译生成字节码文件,然后打包生成.dex时会按照类,方法等进行分类,使用IndexMap结构处理。
所有的方法都存放在short[] 里, 而short的大小正是65K。
method的数量远多于class的数量,method的数量会先到65K,class、field同样存在65k的大小限制。
统计时是把系统的,第三方的加上自己的,方法数量的合,都放到这个数组里进行计算, frameworkMethodids + libraryMdthodids + mycodemethodids。
2、在2.3系统之前,虚拟机内存只分配了5M
使用:
multidex是一个文档齐全的成熟的解决方案。强烈推荐遵循安卓开发者网站上的指示来启用multidex。也可以参考github上的项目样例。
在app/build.gradle 下,添加:
1 compile 'com.android.support:multidex:1.0.1'
在application中:(没有继承其他application的,使用MultidexApplication,如果继承了其他application的,就用如下方式加载)
1 @Override 2 protected void attachBaseContext(Context base) { 3 super.attachBaseContext(base); 4 //因为引用的包过多,实现多包问题 5 MultiDex.install(this); 6 }
Multidex的方式的局限性:
(1)如果DEX文件太大,安装分割dex文件是一个复杂的过程,可能会导致应用程序无响应(ANR)的错误。在这种情况下,你应该尽量的减小dex文件的大小和删除无用的逻辑,而不是完全依赖于multidex。
(2)在Android 4.0设备(API Level 14)之前,由于Dalvik linearalloc bug(问题22586),multidex很可能是无法运行的。如果希望运行在Level 14之前的Android系统版本,请先确保完整的测试和使用。
(3)应用程序使用了multiedex,需要申请很多的一块内存,会造成使用比较大的内存,当然,可能还会引起dalvik虚拟机的崩溃(issue 78035)。
(4)对于应用程序比较复杂的,存在较多的library的项目。multidex可能会造成不同依赖项目间的dex文件函数相互调用,找不到方法。
声明主 DEX 文件中需要的类
为 Dalvik 可执行文件分包构建每个 DEX 文件时,构建工具会执行复杂的决策制定来确定主要 DEX 文件中需要的类,以便应用能够成功启动。如果启动期间需要的任何类未在主 DEX 文件中提供,那么您的应用将崩溃并出现错误 java.lang.NoClassDefFoundError
。
该情况不应出现在直接从应用代码访问的代码上,因为构建工具能识别这些代码路径,但可能在代码路径可见性较低(如使用的库具有复杂的依赖项)时出现。例如,如果代码使用自检机制或从原生代码调用 Java 方法,那么这些类可能不会被识别为主 DEX 文件中的必需项。
因此,如果您收到 java.lang.NoClassDefFoundError
,则必须使用构建类型中的 multiDexKeepFile
或 multiDexKeepProguard
属性声明它们,以手动将这些其他类指定为主 DEX 文件中的必需项。如果类在 multiDexKeepFile
或 multiDexKeepProguard
文件中匹配,则该类会添加至主 DEX 文件。
multiDexKeepFile 属性
您在 multiDexKeepFile
中指定的文件应该每行包含一个类,并且采用 com/example/MyClass.class
的格式。例如,您可以创建一个名为 multidex-config.txt
的文件,如下所示:
1 com/example/MyClass.class 2 com/example/MyOtherClass.class
然后,您可以按以下方式针对构建类型声明该文件:
1 android { 2 buildTypes { 3 release { 4 multiDexKeepFile file 'multidex-config.txt' 5 ... 6 } 7 } 8 }
请记住,Gradle 会读取相对于 build.gradle
文件的路径,因此如果 multidex-config.txt
与 build.gradle
文件在同一目录中,以上示例将有效。
解决ANR的方法:
总体思路是:把这个MultiDex.install(this)放到异步线程中加载,同时又要保证在用户使用功能之前,异步线程已经把非主dex加载完成。
从编译到运行起来,发生什么了呢?
1. 编译生成 allclasses.jar 生成classes.dex, classes2.dex, .... 多个。
安装阶段:把.dex 优化成classes.odex, 注意,只优化第一个dex文件。
首次运行:
执行MultiDex.install()必然会再次对classes2.dex执行dexopt等操作,所有这些操作必须在5秒内完成,否则就ANR。
非首次启动则直接从cache中读取已经执行过dexopt的ODEX文件,这个过程对启动并无太大影响
1. dexpath BaseClassLoader.pathlist 类, 把所有的dex都加载。解决了为什么多个dex里的方法也能被正确的index到,不会导致no such method 的crash
2. odex, 这个过程是非常耗时的,如果第二个dex是5-7M的话大约要加载4s, google这个默认方式时间太长,会导致ANR。
这就需要完成2个事情:
1. 把启动需要加载的类放到主dex中。
2. 显示loading界面,保证用户使用之前把dex加载完成。
主dex中的类:
把这个MultiDex.install(this)放到异步线程中加载,主dex中存放application启动的1直接引用类,所有可能的2入口类,加上3第三方的调用。扫面这些类的直接引用类。统统放到主dex中。
首先在编译打包阶段,在build task中添加一层task, 添加Blacklist.xml自定义的黑名单, 这个名单中的方法对应的类需要添加到主dex中,保证主dex能够加载APP启动时需要的必要的类。
balcklist中包含了需要放到主dex中的类的path,addtask的这个task,把blacklist中的类放到了dextask中生成想要的dex。
如何确定哪些类需要放到blcaklist中:
粒度到方法层面,从主activity加载开始,加载了class B的public x 和public y, 则把B的x 和 y方法中,仅x, y方法中import到的类进行分析, 进行回溯添加。
那如何能让工作更高效,能让查找更有保证呢? 请看下篇,基于字节码文件分析.class文件。
异步加载界面的现实时机:
splash界面,主界面,在收到事件之前进行监听显示load界面。直到异步加载完成所有dex。
其他问题:
主dex不够65k方法,主dex会有多少方法? 生成dex时会一直塞直到塞满65K。
JNI层的回调类,和用到反射的类是需要放到主dex中的,这些BI,库加载都是需要运行就work的。
参考:
第三方库 TurboDex https://github.com/asLody/TurboDex
Android MultiDex 实现原理解析 (配置和简要原理)
Android Dex 分包指南 (Gradle build task)
Android的multidex带来的性能问题-减慢app启动速度 (解决multidex app启动性能问题)
Android关于Dex拆分(MultiDex)技术的解析