Android关于Dex拆分(MultiDex)技术的解析
一、前言
关于Android中的分包技术,已经不是什么新的技术了,网上也有很多解析了,但是他们都是给了理论上的知道和原理解析,并没有详细的案例说明,所以这里我们就来详细讲解一下Android中dex拆分技术的解析。在讲解之前,我们还是先来看一下为什么有这个技术的出现?google为什么提供这样的技术。
二、背景
在开发应用时,随着业务规模发展到一定程度,不断地加入新功能、添加新的类库,代码在急剧的膨胀,相应的apk包的大小也急剧增加, 那么终有一天,你会不幸遇到这个错误:
生成的apk在android 2.3或之前的机器上无法安装,提示:INSTALL_FAILED_DEXOPT
方法数量过多,编译时出错,提示:
Conversion to Dalvik format failed:Unable to execute dex: method ID not in [0, 0xffff]: 65536
无法安装(Android 2.3 INSTALL_FAILED_DEXOPT)问题,是由dexopt的LinearAlloc限制引起的,在Android版本不同分别经历了4M/5M/8M/16M限制,目前主流4.2.x系统上可能都已到16M, 在Gingerbread或者以下系统LinearAllocHdr分配空间只有5M大小的, 高于Gingerbread的系统提升到了8M。Dalvik linearAlloc是一个固定大小的缓冲区。在应用的安装过程中,系统会运行一个名为dexopt的程序为该应用在当前机型中运行做准备。dexopt使用LinearAlloc来存储应用的方法信息。Android 2.2和2.3的缓冲区只有5MB,Android 4.x提高到了8MB或16MB。当方法数量过多导致超出缓冲区大小时,会造成dexopt崩溃。
超过最大方法数限制的问题,是由于DEX文件格式限制,一个DEX文件中method个数采用使用原生类型short来索引文件中的方法,也就是4个字节共计最多表达65536个method,field/class的个数也均有此限制。对于DEX文件,则是将工程所需全部class文件合并且压缩到一个DEX文件期间,也就是Android打包的DEX过程中, 单个DEX文件可被引用的方法总数(自己开发的代码以及所引用的Android框架、类库的代码)被限制为65536。
我们知道原因了,但是我们可以看到,google提供了一个方案:Multidex技术来解决这样的问题,为了兼容老版本SDK,但是我们想一下,是不是所有的项目都会用到这个技术呢?答案肯定不是的,这种问题不是所有的项目都会遇到的,只有当你的项目足够庞大,导致方法个数超出限制了,才会使用到这种技术。那么既然要用到,这里就还是要介绍一下,在介绍这篇文章之前,我们先要准备哪些知识点呢?
1、了解如何使用Ant脚本编译出一个apk包
2、了解编译一个apk包出来的整个流程和步骤
3、了解Android中的dx命令和aapt命令的用法
4、了解Android中动态加载机制
关于这些资料,我在之前的文章中有说道,不了解的同学可以转战:
Ant脚本编译一个apk包以及apk包打包的整个流程和步骤可以查看这篇文章:
http://blog.csdn.net/jiangwei0910410003/article/details/50740026
关于Android中的动态加载机制不了解的同学可以查看这篇文章:
http://blog.csdn.net/jiangwei0910410003/article/details/48104581
只有了解了这四个知识点,下面介绍的内容才能看的明白。所以这两篇文章希望能够仔细看一下。
三、技术原理
既然准备知识已经做完了,下面我们就来详细介绍一下拆包的技术原理,其实就是google提供个方案:
第一步:使用dx进行拆包
这里还有两个两类:一个是自动拆包,一个手动拆包
1、自动拆包
使用google在5.0+的SDK开始,dx命令就已经支持:--multi-dex 参数来直接自动分包
关于具体命令如何进行拆包,后面讲到案例的时候在详细讲解
2、手动拆包
手动拆包其实就是为了解决低版本的SDK,不支持--mulit-dex参数这种情况,所以我们得想个办法,我们知道,android中有一个步骤就是使用dx命令将class文件变成dex,那么这里我们就可以这么做了,在dx命令执行前,先将javac编译之后的所有class文件进行分类,然后在对具体的分类使用dx来转化成dex文件,这样结果也是有多个dex了。这时候我们可能需要写一个额外的脚本去进行class分类了,具体分多少种dex,那就自己决定了。
上面就说完了拆包的两种方式,其实这两种方式各有优势和缺点,当然我们提倡的还是使用第一种方式,因为现在SDK已经是5.0+了,而且这种方式也是google所倡导的,而且也方便。
关于上面的两种分包技术,有一个共同需要注意的地方:
就是Android中在分包之后会有多个dex,但是系统默认会先找到classes.dex文件然后自动加载运行,所以这里就有一个问题,我们需要将一些初始化的重要类放到classes.dex中,不然运行就会报错或者闪退。
那么这里就可以看到这两种分包技术的区别了:
如果使用第一种分包技术的话,我们可以使用:--main-dex-list 参数来规定哪些class文件归到主dex中,也就是classes.dex中,剩余的dx会自动更具方法数的限制来进行分类成从dex,比如classes2.dex...classes3.dex...这里没有classes1.dex。需要注意这点。同时,subdex是不支持class的归类的,完全依靠dx自动分类,这个也是为什么叫做自动拆包的原因吧。
但是如果我们要是使用第二种拆包技术的话,就是很随意的操作了,因为本身分类就是我们自己操作的,所以想怎么分就怎么分,而且这里支持分多少个dex,哪些class归到哪个dex,都是可以做到的。灵活性比较好,但是这种方式有一个不好就是需要自己写脚本去归类,这时候就要非常小心,因为可能会遗漏一个class没有归入到具体的dex的话,运行就会报错,找不到这个类。
第二步:Dex的加载
在第一步中我们讲解完了拆包技术,如何将class拆分成多个dex。那么问题也就来了,上面我们也说到了,Android中在运行的时候默认只会加载classes.dex这个dex文件,我们也叫作主dex.那么拆分之后还有其他的dex怎么加载到系统运行呢?这时候google就提供了一个方案,使用DexClassLoader来进行动态加载,关于动态加载dex的话,这里不想讲解的太多了,因为我之前的很多文章都介绍了,具体可以参考这篇文章:http://blog.csdn.net/jiangwei0910410003/article/details/48104581
所以我们只要获取到所有的dex,除了classes.dex之外。我们然后一一的将各个dex加载到系统中即可。那么这里有两个问题需要解决的:
1、如何获取所有的subdex呢?
这里我们可以这么做,在Application的attachContext方法中(因为这个方法的时机最早),我们可以获取到程序的apk路径,然后使用ZipFile来解压apk文件,取到所有的subdex文件即可,具体的操作看后面的案例详解。
2、我们使用DexClassLoader来加载subdex之后,然后让系统知道?
这里我们使用反射的机制,来合并系统的PathClassLoader和DexClassLoader中的DexList变量值。这个技术也是google提供的。具体技术,也是到后面的案例中我们详细讲解。
四、案例分析
到这里我们就介绍完了,我们拆包的两个步骤:拆分dex和加载dex;下面来使用具体的案例来实践一下吧,这个也是网上现在很多介绍了dex拆分技术,但是就是没有具体案例的不好的地方,理论知识谁都知道,但是没有案例讲解的话,就不知道在实际的过程中会遇到什么问题,以及详细的操作步骤,所以这里必须用一个案例来详细介绍一下。
我们的案例采用ant脚本来编译,其实现在google推荐的是gradle来编译,因为这个已经集成到AndroidStudio中了,说句实话,我是因为gradle的语法不太熟,所以就没用gradle来讲解了,当然gradle语法和ant语法很想,如果有同学会gradle的话,可以使用gradle来进行操作一次,这里我不会太多的介绍ant脚本语法,而是介绍详细的命令使用。其实所有的编译脚本理论上就是对编译命令的优化已经加上一些功能罢了。好了,不多说,看案例:
这里的 main-dex-rule.txt 是我们后面需要用到的主dex包含的class文件清单,my.keystore是签名apk文件
下面我们先来看一下编译脚本build.xml
这里再次说明一下,这里不会全部解读,这个脚本是在我之前的一篇文章:
http://blog.csdn.net/jiangwei0910410003/article/details/50740026
中的用到的脚本基础上修改的,所以一定要先看这篇文章呀~~
首先来看一下如何使用dx命令来自动拆分dex的:
这里的参数很简单:Generate multi-dex...
参数说明:
--multi-dex:多 dex 打包的开关
--main-dex-list=
--minimal-main-dex:只有在--main-dex-list 文件中指定的类被打包在第一个 dex,其余的都在第二个 dex 文件中
因为后两个参数是 optional 参数,所以理论上只需给 dx 加上“--multi-dex”参数即可生成出 classes.dex、classes2.dex、c