记得还是很多年以前,某个新增功能需要使用一个第三方jar包,一顿操作猛入虎准备交活,编译报错了,大致是 dex文件方法数超过了65536 ,自然而然认识了MultiDex,虽然最终是通过proguard代码缩减解决的。
知识回顾:
Android 程序一般使用 Java 语言开发,但是 Dalvik 等虚拟机并不支持直接执行 JAVA 字节码,所以会对编译生成的 .class 文件进行额外处理,最终生成的产物会以 .dex 结尾,称为 Dex 文件。Dex 文件格式是专为 Dalvik 设计的一种压缩格式,但是一个dex文件最多只能有65536个方法,也就是我们常说的64K方法数限制,随着app的使用场景越来越复杂,即使进行了代码缩减,也很难在一个dex 文件中包含全部功能。所以我们需要把app编译成包含多个dex文件,就不会有方法数超过65536的编译错误了。
Android 4.4 及以下采用的Dalvik 虚拟机,只能执行做过 OPT 优化的 DEX 文件,也就是 ODEX 文件。一个 APK 在安装的时候,其中的classes.dex会自动做 ODEX 优化,并在启动的时候由系统默认直接加载到 APP 的PathClassLoader里面,因此classes.dex中的类肯定能直接访问,不需要我们操心。
除它之外的 DEX 文件,也就是classes2.dex、classes3.dex、classes4.dex等 DEX 文件(这里我们统称为 Secondary DEX 文件),这些文件都需要靠我们自己进行 ODEX 优化,并加载到 PathClassLoader 里,才能正常使用其中的类。否则在访问这些类的时候,就会抛出ClassNotFound异常从而引起崩溃。
dex怎么加载到PathClassLoader?app运行时怎么从dex加载类?
文章内容基于Android 34 sdk sources, multidex 2.0.1,当前版本的PathClassLoader类图简化如下,包含了涉及到的类、属性和方法。Dex经过makeDexElements加载后赋值给DexPathList的dexElements,应用的类也是从这个列表按顺序查找加载。
应用启动时系统会创建PathClassLoader,在父类构造函数里初始化了一个DexPathList对象,参数dexPath是可以包含classes和资源的jar/apk/zip/dex文件路径,多个路径用冒号分开, 一般默认就只有 APP的安装包路径,比如:
dexPath= /data/app/~~fmvqN9xs10CVaMI2ZRR4Q==/com.bestjoy.app.tv-KhBnl6h3u9URR4Wo0rdXMQ==/base.apk
DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
// save dexPath for BaseDexClassLoader
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext, isTrusted);
}
makeDexElements方法就是关键,会将dexPath中的每个file调用loadDexFile加载成DexFile,最终形成Element[]赋值给dexElements。
系统创建的PathClassLoader默认optimizedDirectory是null的, 优化后的dex会存放在系统指定的位置,比如dalvik-cache目录, 所以这里直接new DexFile。
openDexFile方法是一个native方法,就不继续分析了,我们就把mCookie当做是打开的dex文件的引用理解好了。
经过上面的步骤,DexPathList.dexElements已经初始化好了,后续运行加载需要的类的链路可以用下图表示:
DexPathList.findClass中dexElements循环调用element.findClass
实际上就是调用的DexFile.loadClassBinaryName
上面这一堆就是要明确一件事,一个类在加载前,其所在的dex文件必须先加载到dexPathList.dexElements中,否则运行时就会报ClassNotFound异常。
为此,Android 官方推出了 MultiDex 方案,它的实现原理就是帮我们解开 APK 包,对第二个以后的 DEX 文件做 ODEX 优化并加载到dexPathList.dexElements里,这样带有多个 DEX 文件的 APK 就能正常运行了。
使用这个库只需要在 APP 程序执行最早的入口,也就是Application.attachBaseContext里面直接调MultiDex.install就够了。
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(base)
}
看上去很简单,实现原理却值得学习,可以借鉴来实现动态加载外部库。
既然apk只有一个classes.dex的时候可以调用系统提供的方法加载到PathClassLoader,那我们就把其他的dex每个单独保存成一个只有单个classes.dex的zip(apk实际上也是zip),然后也调用相同的方法加载并追加到PathClassLoader中去就可以了。
doInstallation实际上也是这样做的,分为MultiDexExtractor.load和installSecondaryDexes两个步骤。
第一步,MultiDexExtractor.load
解压APK里classes2.dex、classes3.dex、classes4.dex等DEX 文件,分别进行 ZIP 压缩,保存为只有单个classes.dex文件条目的classesN.zip, 全部转换后记录下此时sourceApk的crc和时间戳。
比如此时我们可以看到 APP 的 code_cache 目录下会有类似的文件:
/data/app/com.bestjoy.app.tv/code_cache/secondary-dexes/base.apk.classes2.zip
/data/app/com.bestjoy.app.tv/code_cache/secondary-dexes/base.apk.classes3.zip
/data/app/com.bestjoy.app.tv/code_cache/secondary-dexes/base.apk.classes4.zip
生成zips是一个耗时操作,load不会每次都做一遍,会先比较此时sourceApk的crc值和时间戳是否和上次记录的一致,只有不一致才重新生成zips。
生成zips是在performExtractions()方法,从classes2.dex开始, 之后数字每次递增1,查询apk中是否存在该文件条目dexFile,不存在则说明没有dex文件了,转换完成;否则对存在的dexFile调用extract(apk, dexFile, extractedFile, extractedFilePrefix)
经过extract后,APP中的classesN.dex已经保存为base.apk.classesN.zip了。
这些Zip随后会交给下一步处理。
第二步,installSecondaryDexes
知识点回顾里有提到如何调用系统方法加载apk中的classes.dex,这里也一样,只不过我们需要反射调用系统方法。
以V19.install(loader, files, dexDir)为例,我们先再看一下类图, 目标是把第一步那些zip加载追加到DexPathList的dexElements里。
先反射获得当前PathClassLoader对象的pathList值(DexPathList对象), 然后makeDexElements方法会反射调用DexPathListde的makeDexElements方法加载zip。
比如此时我们可以看到 APP 的 code_cache 目录下会有多出来的.dex文件:
/data/app/com.bestjoy.app.tv/code_cache/secondary-dexes/base.apk.classes2.zip
/data/app/com.bestjoy.app.tv/code_cache/secondary-dexes/base.apk.classes2.dex
/data/app/com.bestjoy.app.tv/code_cache/secondary-dexes/base.apk.classes3.zip
/data/app/com.bestjoy.app.tv/code_cache/secondary-dexes/base.apk.classes3.dex
/data/app/com.bestjoy.app.tv/code_cache/secondary-dexes/base.apk.classes4.zip
/data/app/com.bestjoy.app.tv/code_cache/secondary-dexes/base.apk.classes4.dex
接着在expandFieldArray方法里将新加载的dexElements和原本pathList中已有的dexElements合并,合集重新设置给pathList.dexElements。
2024.04.09更新
Multidex在应用安装或更新后首次启动,存在严重的性能问题,用户需要等待相当长的时间才能正常使用APP,所以推荐字节跳动开源的BoostMultidex框架,带来了显著的性能提升,这里是一个对比结果: 有兴趣的可以前往【传送门】。