为什么需要对Dex进行分包
Android在安装应用的过程中,系统会运行一个名为DexOpt的程序为该应用在当前机型中运行做准备。DexOpt 是在第一次加载 Dex 文件的时候执行的。这个过程会生成一个 ODEX 文件,即 Optimised Dex。执行 ODEX 的效率会比直接执行 Dex 文件的效率要高很多。
在开发应用时,随着业务规模发展到一定程度,不断地加入新功能、添加新的类库,代码在急剧的膨胀,相应的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限制引起的。DexOpt使用LinearAlloc来存储应用的方法信息,Dalvik 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;
MultiDex方案
Google为构建超过65K方法数的应用提供官方支持的方案:MultiDex。
首先使用Android SDK Manager升级到最新的Android SDK Build Tools和Android Support Library。然后进行以下两步操作:
- 修改Gradle配置文件,启用MultiDex并包含MultiDex支持:
android {
compileSdkVersion 21
buildToolsVersion "21.1.0"
defaultConfig {
...
minSdkVersion 14
targetSdkVersion 21
...
// Enabling MultiDex support.
MultiDexEnabled true
}
...
}
dependencies { compile 'com.android.support:MultiDex:1.0.0'
}
- 让应用支持多DEX文件。在官方文档中描述了三种可选方法:
- 在AndroidManifest.xml的application中声明android.support.MultiDex.MultiDexApplication;
- 如果你已经有自己的Application类,让其继承MultiDexApplication;
- 如果你的Application类已经继承自其它类,你不想/能修改它,那么可以重写attachBaseContext()方法:
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
Multidex的局限性
官方文档中提到了Multidex的局限性:
1.如果第二个(或其他个)dex文件很大的话,安装.dex文件到data分区时可能会导致ANR(应用程序无响应),此时应该使用ProGuard减小DEX文件的大小。
2.由于Dalvik linearAlloc的bug的关系,使用了multidex的应用可能无法在Android 4.0 (API level 14)或之前版本的设备上运行。
3.由于Dalvik linearAlloc的限制,使用了multidex的应用会请求非常大的内存分配,从而导致程序奔溃。Dalvik linearAlloc是一个固定大小的缓冲区。 在应用的安装过程中,系统会运行一个名为dexopt的程序为该应用在当前机型中运行做准备。dexopt使用LinearAlloc来存储应用的方法信息。 Android 2.2和2.3的缓冲区只有5MB,Android 4.x提高到了8MB或16MB。当方法数量过多导致超出缓冲区大小时,会造成dexopt崩溃。
4.在Dalvik运行时中,某些类的方法必须要放在主dex中,Android构建工具可能无法确保所有有此要求的类被编译进主dex中。
这些问题也非常值得我们关注。
一些在二级Dex加载之前,可能会被调用到的类(比如静态变量的类),需要放在主Dex中,否则会ClassNotFoundError。 通过修改Gradle,可以显式的把一些类放在Main Dex中。
afterEvaluate {
tasks.matching {
it.name.startsWith('dex')
}.each { dx ->
if (dx.additionalParameters == null) {
dx.additionalParameters = []
}
dx.additionalParameters += '--multi-dex'
dx.additionalParameters += "--main-dex-list=$projectDir/<filename>".toString()
}
}
注意上面是修改后的Gradle,其中是一个文本文件的文件名,存放在和这个build.gradle脚本同一级的文件目录下,而不是项目根目录。可以把这个文本文件起名为multidex.keep,内容如下,实际就是把需要放在Main Dex的类罗列出来。
android/support/multidex/BuildConfig/class
android/support/multidex/MultiDex$V14/class
android/support/multidex/MultiDex$V19/class
android/support/multidex/MultiDex$V4/class
android/support/multidex/MultiDex/class
android/support/multidex/MultiDexApplication/class
android/support/multidex/MultiDexExtractor$1/class
android/support/multidex/MultiDexExtractor/class
android/support/multidex/ZipUtil$CentralDirectory/class
android/support/multidex/ZipUtil/class
project.afterEvaluate标签在特定的project配置完成后运行,而gradle.projectsEvaluated在所有projects配置完成后运行。 注意afterEvaluate需要放在android{}里,不可放外面。
这样做了之后并不一定解压apk之后会出现多个dex文件,可能仍然只有一个dex。因为只有必须分包的时候才会分,如果不需要就不会。 如果要强制分dex,还需要加上dx.additionalParameters += ‘–minimal-main-dex’。完整的配置如下:
afterEvaluate {
tasks.matching {
it.name.startsWith('dex')
}.each { dx ->
if (dx.additionalParameters == null) {
dx.additionalParameters = []
}
dx.additionalParameters += '--multi-dex'
// 设置multidex.keep文件中class为第一个dex文件中包含的class,如果没有下一项设置此项无作用
dx.additionalParameters += "--main-dex-list=$projectDir/multidex.keep".toString()
//此项添加后第一个classes.dex文件只能包含-main-dex-list列表中class
dx.additionalParameters += '--minimal-main-dex'
}
}
这样配置了之后就按照multidex.keep里面的内容拆分出了第一个dex文件。其他内容在第二个里面。 那么如何把需要的类放在multidex.keep文件里呢?其实不用手动一个类一个类写,我们进入这个文件: 项目\build\intermediates\multi-dex\release(或debug)\maindexlist.txt。 将maindexlist.txt中没有在application中初始化的类删除一部分之后,剩余的复制到multidex.keep文件中就可以了。 当然也可以自行增加没有被包含进去的类,因为不直接引用的类都不在maindexlist.txt中。 注意,如果需要混淆的话需要写混淆之后的 class 。
MultiDex实现原理
1.Dex拆分
dex拆分步骤为:
- 自动扫描整个工程代码得到main-dex-list;
- 根据main-dex-list对整个工程编译后的所有class进行拆分,将主、从dex的class文件分开;
- 用dx工具对主、从dex的class文件分别打包成 .dex文件,并放在apk的合适目录。
怎么自动生成 main-dex-list? Android SDK 从 build tools 21 开始提供了 mainDexClasses 脚本来生成主 dex 的文件列表。查看这个脚本的源码,可以看到它主要做了下面两件事情:
1)调用 proguard 的 shrink 操作来生成一个临时 jar 包;
2)将生成的临时 jar 包和输入的文件集合作为参数,然后调用com.android.multidex.MainDexListBuilder 来生成主 dex 文件列表。
2.Dex加载
因为Android系统在启动应用时只加载了主dex(Classes.dex),其他的 dex 需要我们在应用启动后进行动态加载安装。android-support-multidex.jar就是做这个用的,该 jar 包从 build tools 21.1 开始支持。
android系统使用BaseDexClassLoader来加载Dex文件,它有两个子类DexClassLoader和PathClassLoader,它们使用场景如下:
- PathClassLoader是Android应用中的默认加载器,PathClassLoader只能加载/data/app中的apk,也就是已经安装到手机中的apk。这个也是PathClassLoader作为默认的类加载器的原因,因为一般程序都是安装了,在打开,这时候PathClassLoader就去加载指定的apk(解压成dex,然后在优化成odex)就可以了。
- DexClassLoader可以加载任何路径的apk/dex/jar,PathClassLoader只能加载已安装到系统中(即/data/app目录下)的apk文件。
基本实现原理:
1、除了第一个dex文件(即正常apk包唯一包含的Dex文件),其它dex文件都以资源的方式放在安装包中。所以我们需要将其他dex文件并在Application的onCreate回调中注入到系统的ClassLoader。并且对于那些在注入之前已经引用到的类(以及它们所在的jar),必须放入第一个Dex文件中。
2、PathClassLoader作为默认的类加载器,在打开应用程序的时候PathClassLoader就去加载指定的apk(解压成dex,然后在优化成odex),也就是第一个dex文件是PathClassLoader自动加载的。所以,我们需要做的就是将其他的dex文件注入到这个PathClassLoader中去。
3、因为PathClassLoader和DexClassLoader的原理基本一致,从前面的分析来看,我们知道PathClassLoader里面的dex文件是放在一个Element数组里面,可以包含多个dex文件,每个dex文件是一个Element,所以我们只需要将其他的dex文件放到这个数组中去就可以了。
实现:
1、通过反射获取PathClassLoader中的DexPathList中的Element数组(已加载了第一个dex包,由系统加载)
2、通过反射获取DexClassLoader中的DexPathList中的Element数组(将第二个dex包加载进去)
3、将两个Element数组合并之后,再将其赋值给PathClassLoader的Element数组
网友评论