Android multidex 使用 与 实现原理
Android multidex 使用 与 实现原理
在Android中一个Dex文件最多存储65536个方法,也就是一个short类型的范围。但随着应用方法数量的不断增加,当Dex文件突破65536方法数量时,打包时就会抛出异常。
为解决该问题,Android5.0时Google推出了官方解决方案:MultiDex。
- 打包时,把一个应用分成多个dex,例:classes.dex、classes2.dex、classes3.dex...,加载的时候把这些dex都追加到DexPathList对应的数组中,这样就解决了方法数的限制。
- Andorid 5.0之后,ART虚拟机天然支持MultiDex。
- Andorid 5.0之前,系统只加载一个主dex,其它的dex采用MultiDex手段来加载。
一、使用
如何使用,最好参照google官方文档,写的很详细:
这里做一下简要说明:
1、minSdkVersion 为 21 或更高值
如果是android 5.0
以上的设备,只需要设置为multiDexEnabled true
android {
defaultConfig {
...
minSdkVersion 21
targetSdkVersion 26
multiDexEnabled true
}
...
}
2、minSdkVersion 为 20 或更低值
如果需要适配android 5.0
以下的设备,需使用 Dalvik 可执行文件分包支持库
android {
defaultConfig {
...
minSdkVersion 15
targetSdkVersion 26
multiDexEnabled true
}
...
}
dependencies {
compile 'com.android.support:multidex:1.0.3'
}
Java代码方面,继承MultiDexApplication
或者 在Application
中添加MultiDex.install(this);
// 继承 MultiDexApplication
public class MyApplication extends MultiDexApplication { ... }
// 或者 在Application中添加 MultiDex.install(this);
public class MyApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
}
二、android 5.0 以下 MultiDex 原理
注:
源码基于的版本 com.android.support:multidex:1.0.3
通过 Dalvik可执行文件分包支持库 和 配置方法数超过64K的应用 我们了解到:
-
android 5.0
以下Dalvik虚拟机
只能加载一个主class.dex
; -
android.support.multidex.MultiDex.install(this)
是对android 5.0
以下Dalvik虚拟机
的兼容;
这里我们分两部分介绍,一部分是dex文件的加载;一部分是dex文件的抽取。
2.1、Dex文件的加载
下面通过跟踪 MultiDex.install(this);
源码,了解其实现原理。
MultiDex.install(this);
跟踪 MultiDex.install(this);
源码
public static void install(Context context) {
// 如果系统版本大于android 5.0 则天然支持MultiDex
if (IS_VM_MULTIDEX_CAPABLE) {
Log.i("MultiDex", "VM has multidex support, MultiDex support library is disabled.");
}
// 系统版本低于android 1.6 抛出异常
else if (VERSION.SDK_INT < 4) {
throw new RuntimeException("MultiDex installation failed. SDK " + VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + ".");
}
// android 1.6 < android < android 5.0
else {
try {
// 获取当前应用信息 应用信息不存在,则返回
ApplicationInfo applicationInfo = getApplicationInfo(context);
if (applicationInfo == null) {
Log.i("MultiDex", "No ApplicationInfo available, i.e. running on a test Context: MultiDex support library is disabled.");
return;
}
// MultiDex
// sourceDir: /data/app/com.xiaxl.demo-2/base.apk
// dataDir: /data/user/0/com.xiaxl.demo
doInstallation(context, new File(applicationInfo.sourceDir), new File(applicationInfo.dataDir), "secondary-dexes", "", true);
} catch (Exception var2) {
Log.e("MultiDex", "MultiDex installation failure", var2);
throw new RuntimeException("MultiDex installation failed (" + var2.getMessage() + ").");
}
Log.i("MultiDex", "install done");
}
}
上边代码中,对1.6 < android < android 5.0 进行判断处理,低于1.6版本抛出异常;高于5.0版本,天然支持MultiDex,所以忽略
- 如果系统版本大于android 5.0
ART虚拟机
天然支持MultiDex - 系统版本低于android 1.6 抛出异常
- doInstallation MultiDex 处理
跟踪 MultiDex.doInstallation
跟踪 MultiDex.doInstallation,查看MultiDex的实现原理
// 相关入口参数
// sourceDir: /data/app/com.xiaxl.demo-2/base.apk
// dataDir: /data/user/0/com.xiaxl.demo
// secondaryFolderName: "secondary-dexes"
// prefsKeyPrefix: ""
// reinstallOnPatchRecoverableException: true
private static void doInstallation(Context mainContext, File sourceApk, File dataDir, String secondaryFolderName, String prefsKeyPrefix, boolean reinstallOnPatchRecoverableException) throws IOException, IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, SecurityException, ClassNotFoundException, InstantiationException {
// 已安装Apk
Set var6 = installedApk;
// 同步
synchronized(installedApk) {
// 如果 /data/app/com.xiaxl.demo-2/base.apk 未安装
if (!installedApk.contains(sourceApk)) {
// 添加到 installedApk 这个集合中
installedApk.add(sourceApk);
// Android 系统版本大约5.0("java.vm.version"的版本号错误),天然支持MultiDex
if (VERSION.SDK_INT > 20) {
Log.w("MultiDex", "MultiDex is not guaranteed to work in SDK version " + VERSION.SDK_INT + ": SDK version higher than " + 20 + " should be backed by " + "runtime with built-in multidex capabilty but it's not the " + "case here: java.vm.version=\"" + System.getProperty("java.vm.version") + "\"");
}
// 根据context 获取 ClassLoader
ClassLoader loader;
try {
// 获取ClassLoader,实际上是PathClassLoader
loader = mainContext.getClassLoader();
} catch (RuntimeException var25) {
Log.w("MultiDex", "Failure while trying to obtain Context class loader. Must be running in test mode. Skip patching.", var25);
return;
}
// ClassLoader 获取失败
if (loader == null) {
Log.e("MultiDex", "Context class loader is null. Must be running in test mode. Skip patching.");
}
//
else {
// 清除老的缓存的Dex目录,来源的缓存目录是"/data/user/0/${packageName}/files/secondary-dexes"
// 清空 /data/user/0/com.xiaxl.demo/files/secondary-dexes
try {
clearOldDexDir(mainContext);
} catch (Throwable var24) {
Log.w("MultiDex", "Something went wrong when trying to clear old MultiDex extraction, continuing without cleaning.", var24);
}
// 新建一个存放dex的目录,路径是"/data/user/0/${packageName}/code_cache/secondary-dexes",用来存放优化后的dex文件
// 创建 /data/user/0/com.xiaxl.demo/code_cache/secondary-dexes 目录
File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);
// 使用MultiDexExtractor这个工具类把APK中的dex抽取到dexDir目录中,返回的files集合有可能为空,表示没有secondaryDex
// 不强制重新加载,也就是说如果已经抽取过了,可以直接从缓存目录中拿来使用,这么做速度比较快
// sourceApk: /data/app/com.xiaxl.demo-2/base.apk
// dexDir: /data/user/0/com.xiaxl.demo/code_cache/secondary-dexes
MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir);
IOException closeException = null;
try {
// prefsKeyPrefix: ""
// 返回dex文件列表
List files = extractor.load(mainContext, prefsKeyPrefix, false);
try {
// 安装secondaryDex
// /data/user/0/com.xiaxl.demo/code_cache/secondary-dexes
installSecondaryDexes(loader, dexDir, files);
} catch (IOException var26) {
if (!reinstallOnPatchRecoverableException) {
throw var26;
}
Log.w("MultiDex", "Failed to install extracted secondary dex files, retrying with forced extraction", var26);
files = extractor.load(mainContext, prefsKeyPrefix, true);
installSecondaryDexes(loader, dexDir, files);
}
} finally {
try {
extractor.close();
} catch (IOException var23) {
closeException = var23;
}
}
if (closeException != null) {
throw closeException;
}
}
}
}
}
忽略dex文件抽取逻辑和校验逻辑,以上代码中主要做了以下三件事:
- 清空缓存目录"/data/user/0/${packageName}/files/secondary-dexes"
- 使用MultiDexExtractor这个工具把APK中的dex抽取到"/data/user/0/${packageName}/code_cache/secondary-dexes"目录
- 加载"/data/user/0/${packageName}/code_cache/secondary-dexes"目录下的dex
下边查看MultiDex.installSecondaryDexes
方法,了解MultiDex
的具体实现
MultiDex.V4.install(loader, files);
// dexDir: /data/user/0/com.xiaxl.demo/code_cache/secondary-dexes
private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<? extends File> files) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException, SecurityException, ClassNotFoundException, InstantiationException {
if (!files.isEmpty()) {
if (VERSION.SDK_INT >= 19) {
MultiDex.V19.install(loader, files, dexDir);
} else if (VERSION.SDK_INT >= 14) {
MultiDex.V14.install(loader, files);
} else {
MultiDex.V4.install(loader, files);
}
}
}
不同版本的Android系统,类加载机制有一些不同,所以分为了V19、V14和V4
三种情况下的安装。
这里我们看下一V19的源码
private static final class V19 {
private V19() {
}
// additionalClassPathEntries: dex列表
// optimizedDirectory: /data/user/0/com.xiaxl.demo/code_cache/secondary-dexes
static void install(ClassLoader loader, List<? extends File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
// 传递的loader是PathClassLoader,findFidld()方法找到父类BaseClassLoader中pathList属性
// 获取BaseDexClassLoader中pathList属性
// this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
Field pathListField = MultiDex.findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
// 将dex文件添加到DexPathList中的dexElements 数组的末尾
ArrayList<IOException> suppressedExceptions = new ArrayList();
MultiDex.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory, suppressedExceptions));
// 后面就是添加一些IO异常信息,因为调用DexPathList的makeDexElements会有一些IO操作,相应的可能就会有一些异常情况
if (suppressedExceptions.size() > 0) {
Iterator var6 = suppressedExceptions.iterator();
while(var6.hasNext()) {
IOException e = (IOException)var6.next();
Log.w("MultiDex", "Exception in makeDexElement", e);
}
Field suppressedExceptionsField = MultiDex.findField(dexPathList, "dexElementsSuppressedExceptions");
IOException[] dexElementsSuppressedExceptions = (IOException[])((IOException[])suppressedExceptionsField.get(dexPathList));
if (dexElementsSuppressedExceptions == null) {
dexElementsSuppressedExceptions = (IOException[])suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
} else {
IOException[] combined = new IOException[suppressedExceptions.size() + dexElementsSuppressedExceptions.length];
suppressedExceptions.toArray(combined);
System.arraycopy(dexElementsSuppressedExceptions, 0, combined, suppressedExceptions.size(), dexElementsSuppressedExceptions.length);
dexElementsSuppressedExceptions = combined;
}
suppressedExceptionsField.set(dexPathList, dexElementsSuppressedExceptions);
IOException exception = new IOException("I/O exception during makeDexElement");
exception.initCause((Throwable)suppressedExceptions.get(0));
throw exception;
}
}
// 通过反射的方式调用DexPathList#makeDexElements()方法
// dexPathList: DexPathList
// files: dex文件列表
private static Object[] makeDexElements(Object dexPathList, ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
// 通过DexPathList的makeDexElements方法加载 “dex文件”
Method makeDexElements = MultiDex.findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class, ArrayList.class);
return (Object[])((Object[])makeDexElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions));
}
}
通过V19的install()方法,关于MultiDex如何加载Dex文件的问题已经清晰:
- 将APK文件中除主dex文件之外的dex文件追加到
PathClassLoader(也就是BaseClassLoader)
中DexPathListde Element[]
数组中。这样在加载一个类的时候就会遍历所有的dex文件,保证了打包的类都能够正常加载。
### 2.2、Dex文件的抽取
前边说过:
`MultiDexExtractor`这个工具类的作用是把APK中的`dex`文件抽取到`/data/user/0/com.xiaxl.demo/code_cache/secondary-dexes`目录中
#### MultiDexExtractor 构造方法
```java
// sourceApk: /data/app/com.xiaxl.demo-2/base.apk
// dexDir: /data/user/0/com.xiaxl.demo/code_cache/secondary-dexes
MultiDexExtracto(File sourceApk, File dexDir) throws IOException {
this.sourceApk = sourceApk;
this.dexDir = dexDir;
// 循环冗余校验码(CRC)
this.sourceCrc = getZipCrc(sourceApk);
// 创建 /data/user/0/com.xiaxl.demo/code_cache/secondary-dexes/MultiDex.lock
File lockFile = new File(dexDir, "MultiDex.lock");
// 对文件内容的访问,既可以读文件也可以写文件,可以访问文件的任意位置适用于由大小已知的记录组成的文件
// 对/data/user/0/com.xiaxl.demo/code_cache/secondary-dexes/MultiDex.lock 进行读写
this.lockRaf = new RandomAccessFile(lockFile, "rw");
try {
// 返回文件通道
this.lockChannel = this.lockRaf.getChannel();
try {
Log.i("MultiDex", "Blocking on lock " + lockFile.getPath());
this.cacheLock = this.lockChannel.lock();
} catch (RuntimeException | Error | IOException var5) {
closeQuietly(this.lockChannel);
throw var5;
}
Log.i("MultiDex", lockFile.getPath() + " locked");
} catch (RuntimeException | Error | IOException var6) {
closeQuietly(this.lockRaf);
throw var6;
}
}
```
#### MultiDexExtractor.load
APK中的`dex`文件的抽取
```java
// 返回dex文件列表
// prefsKeyPrefix: ""
// forceReload: false
List<? extends File> load(Context context, String prefsKeyPrefix, boolean forceReload) throws IOException {
// MultiDexExtractor 不可用
if (!this.cacheLock.isValid()) {
throw new IllegalStateException("MultiDexExtractor was closed");
} else {
List files;
// forceReload ==false;
// isModified == true;
// 如果不需要重新加载并且文件没有被修改过
// isModified()方法是根据SharedPreference中存放的APK文件上一次修改的时间戳和currentCrc来判断是否修改过文件
if (!forceReload && !isModified(context, this.sourceApk, this.sourceCrc, prefsKeyPrefix)) {
try {
// 从缓存目录中加载已经抽取过的文件,并返回dex文件列表
files = this.loadExistingExtractions(context, prefsKeyPrefix);
} catch (IOException var6) {
Log.w("MultiDex", "Failed to reload existing extracted secondary dex files, falling back to fresh extraction", var6);
files = this.performExtractions();
putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files);
}
} else {
if (forceReload) {
Log.i("MultiDex", "Forced extraction must be performed.");
} else {
Log.i("MultiDex", "Detected that extraction must be performed.");
}
// 如果强制加载或者APK文件已经修改过就重新抽取dex文件
files = this.performExtractions();
putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files);
}
Log.i("MultiDex", "load found " + files.size() + " secondary dex files");
return files;
}
}
```
#### MultiDexExtractor.performExtractions()
```java
private List<MultiDexExtractor.ExtractedDex> performExtractions() throws IOException {
// 抽取出的dex文件名前缀是"base.apk.classes"
String extractedFilePrefix = this.sourceApk.getName() + ".classes";
this.clearDexDir();
// 返回的dex列表
List<MultiDexExtractor.ExtractedDex> files = new ArrayList();
// apk压缩包
ZipFile apk = new ZipFile(this.sourceApk);
try {
int secondaryNumber = 2;
for(ZipEntry dexFile = apk.getEntry("classes" + secondaryNumber + ".dex"); dexFile != null; dexFile = apk.getEntry("classes" + secondaryNumber + ".dex")) {
// base.apk.classes2.zip
String fileName = extractedFilePrefix + secondaryNumber + ".zip";
// 创建文件/data/app/com.xiaxl.demo-2/base.apk.classes2.zip
MultiDexExtractor.ExtractedDex extractedFile = new MultiDexExtractor.ExtractedDex(this.dexDir, fileName);
// 添加到文件列表
files.add(extractedFile);
Log.i("MultiDex", "Extraction is needed for file " + extractedFile);
int numAttempts = 0;
boolean isExtractionSuccessful = false;
// 抽取dex
while(numAttempts < 3 && !isExtractionSuccessful) {
++numAttempts;
extract(apk, dexFile, extractedFile, extractedFilePrefix);
try {
extractedFile.crc = getZipCrc(extractedFile);
isExtractionSuccessful = true;
} catch (IOException var18) {
isExtractionSuccessful = false;
Log.w("MultiDex", "Failed to read crc from " + extractedFile.getAbsolutePath(), var18);
}
Log.i("MultiDex", "Extraction " + (isExtractionSuccessful ? "succeeded" : "failed") + " '" + extractedFile.getAbsolutePath() + "': length " + extractedFile.length() + " - crc: " + extractedFile.crc);
if (!isExtractionSuccessful) {
extractedFile.delete();
if (extractedFile.exists()) {
Log.w("MultiDex", "Failed to delete corrupted secondary dex '" + extractedFile.getPath() + "'");
}
}
}
if (!isExtractionSuccessful) {
throw new IOException("Could not create zip file " + extractedFile.getAbsolutePath() + " for secondary dex (" + secondaryNumber + ")");
}
++secondaryNumber;
}
} finally {
try {
apk.close();
} catch (IOException var17) {
Log.w("MultiDex", "Failed to close resource", var17);
}
}
return files;
}
```
### 2.3、其他相关代码
#### clearOldDexDir(Context context)
```java
private static void clearOldDexDir(Context context) throws Exception {
// /data/user/0/com.xiaxl.demo/files/secondary-dexes
File dexDir = new File(context.getFilesDir(), "secondary-dexes");
if (dexDir.isDirectory()) {
Log.i("MultiDex", "Clearing old secondary dex dir (" + dexDir.getPath() + ").");
// 获取文件列表
File[] files = dexDir.listFiles();
// 文件为空
if (files == null) {
Log.w("MultiDex", "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
return;
}
// 文件不为空
File[] var3 = files;
int var4 = files.length;
// 循环清空 /data/user/0/com.xiaxl.demo/files/secondary-dexes 下全部文件
for(int var5 = 0; var5 < var4; ++var5) {
File oldFile = var3[var5];
Log.i("MultiDex", "Trying to delete old file " + oldFile.getPath() + " of size " + oldFile.length());
if (!oldFile.delete()) {
Log.w("MultiDex", "Failed to delete old file " + oldFile.getPath());
} else {
Log.i("MultiDex", "Deleted old file " + oldFile.getPath());
}
}
// 删除 /data/user/0/com.xiaxl.demo/files/secondary-dexes 文件夹
if (!dexDir.delete()) {
Log.w("MultiDex", "Failed to delete secondary dex dir " + dexDir.getPath());
} else {
Log.i("MultiDex", "Deleted old secondary dex dir " + dexDir.getPath());
}
}
}
```
```java
private static File getDexDir(Context context, File dataDir, String secondaryFolderName) throws IOException {
// 创建 /data/user/0/com.xiaxl.demo/code_cache 目录
File cache = new File(dataDir, "code_cache");
try {
mkdirChecked(cache);
} catch (IOException var5) {
cache = new File(context.getFilesDir(), "code_cache");
mkdirChecked(cache);
}
// 创建 /data/user/0/com.xiaxl.demo/code_cache/secondary-dexes 目录
File dexDir = new File(cache, secondaryFolderName);
mkdirChecked(dexDir);
return dexDir;
}
```
## 三、总结
到这里,MultiDex安装多个dex的原理已经清楚了。
+ 通过一定的方式把dex文件抽取出来;
+ 把这些`dex文件追加到DexPathList的Element[]数组的后面`;
这个过程要尽可能的早,所以一般是在Application的attachBaseContext()方法中。
另外,`hotfix`热修复技术,就是通过一定的方式`把修复后的dex插入到DexPathList的Element[]数组前面`实现修复后的class抢先加载。
## 参考:
[Android源代码](https://www.androidos.net.cn/sourcecode)
[类加载机制系列3——MultiDex原理解析](https://juejin.im/entry/5a3a21fcf265da430d58294e)