Multidex 是为了解决应用程序函数数超过65k而出现的,主要是通过打包时把函数拆分到多个 dex 文件,并在程序启动的时候再动态的读入的方式来解决问题, 详细的描述和 Multidex 的使用方式可以参考官网的这篇文章
https://developer.android.com/tools/building/multidex.html。
本文从源码的角度看看 Multidex 究竟做了什么, 整个项目只有 4 个 java 文件。
使用 Multidex 的时候, 只需要直接继承 MultiDexApplication :
public class MultiDexApplication extends Application { @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); MultiDex.install(this); } }
在 attachBaseContext 中直接调用了 MultiDex.install。 attachBaseContext 这个函数, 是在应用程序启动过程中最早被系统触发到的可以做点事情的回调, MultiDex 的逻辑写在这里, 才能保证在应用程序启动之前把所有的 dex 全部读入,保证程序完整。
ZipUtil 里面是几个 crc32 校验的工具函数, 不是重点, 略过、
那重点就是 MultiDex.java 和 MultiDexExtractor.java 这两个文件。
MultiDex.install 首先是做了 两件事情, 一个是检查你当前的虚拟机是不是原生就支持 MultiDex , 如果自带支持的就直接返回了,虚拟机自己搞定; 另外是检查系统版本,小于 4 的不支持, 直接返回。 大于20 会抛个警告给你看看, 但是程序还接着跑。
注:有的童鞋可能不太清楚,Android 4.4 引入了 ART 运行时,而 ART 原生就是支持 MultiDex 的, 所以这里先判断一下设备是不是 ART 运行时,如果不是才有必要执行后续的程序。 至于如何判断出来当前的运行时版本, 官网有这么一段说明:
You can verify which runtime is in use by calling
System.getProperty("java.vm.version")
. If ART is in use, the property's value is
"2.0.0"
or higher.
一通检查过后,来到了 MultiDex 的核心代码:
File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME);
List<File> files = MultiDexExtractor.load(context, applicationInfo, dexDir, false);
if (checkValidZipFiles(files)) {
installSecondaryDexes(loader, dexDir, files);
} else {
Log.w(TAG, "Files were not valid zip files. Forcing a reload.");
// Try again, but this time force a reload of the zip file.
files = MultiDexExtractor.load(context, applicationInfo, dexDir, true);
if (checkValidZipFiles(files)) {
installSecondaryDexes(loader, dexDir, files);
} else {
// Second time didn't work, give up
throw new RuntimeException("Zip files were not valid.");
}
}
做了两件事:
(1) 借助 MultiDexExtractor.load 读取了一个 List<File>
(2) 把上面获取到的 List<File>, 连同当前使用的 ClassLoader ,一起传入 installSecondaryDexes
下面就分别看下 这两步核心的代码:
一、MultiDexExtractor.load
List<File> files;
if (!forceReload && !isModified(context, sourceApk, currentCrc)) {
try {
files = loadExistingExtractions(context, sourceApk, dexDir);
} catch (IOException ioe) {
Log.w(TAG, "Failed to reload existing extracted secondary dex files,"
+ " falling back to fresh extraction", ioe);
files = performExtractions(sourceApk, dexDir);
putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
}
} else {
Log.i(TAG, "Detected that extraction must be performed.");
files = performExtractions(sourceApk, dexDir);
putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
}
这里是两个分支, 一个是读取已经解压出来的包含 dex 的压缩文件-loadExistingExtractions ,另一个是现在去解压 - performExtractions
(1) performExtractions
直接把 apk 文件作为 ZipFile 打开, 遍历找到里面名字是 “classesX.dex” ( X = 数字 ) 的文件 , 重新创建一个 Zip 压缩包, 并把这个 dex 文件改名为 ”classes.dex” 再存到新的 zip 包里, 就是把名字里面的数字扣掉了, 同时, 新创建 出来的, 只包含一个 classes.dex 文件的压缩包, 会被添加到一个 List<File> 里面, 最后返回。
(2)loadExistingExtractions
这个就简单多了, 直接去指定的目录里面, 把已经解压好的、只包含一个 classes.dex 文件的 zip 纪录到一个 List<File> 中, 然后返回。
到这里, 第一部 load 就完成了, 现在拿到了一个 List<File> 集合, 每一个 File 是 一个 zip 包,包中只有一个从当前 apk 文件中提取出来的 classes.dex 文件。
二、installSecondaryDexes
这个代码结构在 Android 源码的 XxxCompat 中经常出现,很经典, 一定要贴出来看看、、
private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<File> files)
throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
InvocationTargetException, NoSuchMethodException, IOException {
if (!files.isEmpty()) {
if (Build.VERSION.SDK_INT >= 19) {
V19.install(loader, files, dexDir);
} else if (Build.VERSION.SDK_INT >= 14) {
V14.install(loader, files, dexDir);
} else {
V4.install(loader, files);
}
}
}
这里就是最后执行 install 操作的入口了, 从代码上就能看出来, 在 api 14 和 19 这两个版本中,官方对 dex 加载的地方做了一些调整, 所以根据不同的版本做分别实现,挑个老的下手吧,看下 V4.install
V4.install, 函数很简单, 总共没几行我就全部贴出来了
private static final class V4 {
private static void install(ClassLoader loader, List<File> additionalClassPathEntries)
throws IllegalArgumentException, IllegalAccessException,
NoSuchFieldException, IOException {
/* The patched class loader is expected to be a descendant of
* dalvik.system.DexClassLoader. We modify its
* fields mPaths, mFiles, mZips and mDexs to append additional DEX
* file entries.
*/
int extraSize = additionalClassPathEntries.size();
Field pathField = findField(loader, "path");
StringBuilder path = new StringBuilder((String) pathField.get(loader));
String[] extraPaths = new String[extraSize];
File[] extraFiles = new File[extraSize];
ZipFile[] extraZips = new ZipFile[extraSize];
DexFile[] extraDexs = new DexFile[extraSize];
for (ListIterator<File> iterator = additionalClassPathEntries.listIterator();
iterator.hasNext();) {
File additionalEntry = iterator.next();
String entryPath = additionalEntry.getAbsolutePath();
path.append(':').append(entryPath);
int index = iterator.previousIndex();
extraPaths[index] = entryPath;
extraFiles[index] = additionalEntry;
extraZips[index] = new ZipFile(additionalEntry);
extraDexs[index] = DexFile.loadDex(entryPath, entryPath + ".dex", 0);
}
pathField.set(loader, path.toString());
expandFieldArray(loader, "mPaths", extraPaths);
expandFieldArray(loader, "mFiles", extraFiles);
expandFieldArray(loader, "mZips", extraZips);
expandFieldArray(loader, "mDexs", extraDexs);
}
}
可以看到这里只干了一件事, 通过反射, 拿到 classloader 中的 “path” 属性, 然后把上一步 拿到的 List<File> 中 File 的路径全部拼到”path”里面, 中间用 冒号分割。
“path” 里面添加了内容以后,相应的 “mPaths”, “mFiles”, “mZips” 和 “mDexs” 这几个数组属性的内容也要与 “path”对应, expandFieldArray 这个函数就是通过反射 获取上述几个属性, 构造出所需的数据结构, 然后塞到各个 属性数组中,到这里 MultiDex 的所有工作就结束了。
注: V14 和 V19 中做的事情跟 V4 都差不多, 就是属性变了变名字, 构造数组直接使用了新版本中添加的函数,这里就不再赘述了。
总结
一个首次安装的程序启动时,MultiDex 的整个工作过程, 概括起来就是两句话:
1、把 apk 中的 classesX.dex 文件解压出来, 单独压缩到独立的 zip 压缩包中, 并把这些压缩包的路径包装在一个 List 中返回。
2、通过反射, 修改 classloader 中的 “path” 属性(新版中是“pathList”),把上一步的到的所有 zip 的路径拼进去。