MultiDex源码解析

MultiDex对于很多开发者来说应该并不陌生,大一点的项目应该都已经遇到了INSTALL_FAILED_DEXOPT 的问题,因为当 Android 系统安装一个应用的时候,有一步是对 Dex 进行优化,这个过程有一个专门的工具来处理,叫 DexOpt。DexOpt 是在第一次加载 Dex 文件的时候执行的。这个过程会生成一个 ODEX 文件,即 Optimised Dex。执行 ODEX 的效率会比直接执行 Dex 文件的效率要高很多。

导致这个错误的原因有两个

  1. 单个 dex 文件方法总数 65K 的限制。

    DexOpt 会把每一个类的方法 id 检索起来,存在一个链表结构里面,但是这个链表的长度是用一个 short 类型来保存的,导致了方法 id 的数目不能够超过65536个。当一个项目足够大的时候,显然这个方法数的上限是不够的。

  2. DexOpt 的 LinearAlloc 限制。issue:22586issue:78035

    Dexopt 使用 LinearAlloc 来存储应用的方法信息。Dalvik LinearAlloc 是一个固定大小的缓冲区。在Android 版本的历史上,LinearAlloc 分别经历了4M/5M/8M/16M限制。Android 2.2和2.3的缓冲区只有5MB,Android 4.x提高到了8MB 或16MB。当方法数量过多导致超出缓冲区大小时,也会造成dexopt崩溃

对此Android官方给出的解决方案就是采用MultiDex方案,关于MultiDex的使用方法,这里就不说了,本文主要从源码的角度来分析MultiDex的的整个工作流程

首先我们从入口方法MultiDex.install(this)开始入手

1
2
3
4
5
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}

我们进入这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public static void install(Context context) {
//在使用ART虚拟机的设备上(部分4.4设备,5.0+以上都默认ART环境),已经原生支持多Dex,因此就不需要手动支持了
if (IS_VM_MULTIDEX_CAPABLE) {
return;
}
...
try {
ApplicationInfo applicationInfo = getApplicationInfo(context);
if (applicationInfo == null) {
// Looks like running on a test Context, so just return without patching.
return;
}
synchronized (installedApk) {
String apkPath = applicationInfo.sourceDir;
if (installedApk.contains(apkPath)) {
return;
}
installedApk.add(apkPath);
ClassLoader loader;
try {
loader = context.getClassLoader();
} catch (RuntimeException e) {
return;
}
if (loader == null) {
return;
}
try {
//清除之前缓存的dex文件(data/data/<packagename>/secondary-dexes)
clearOldDexDir(context);
} catch (Throwable t) {
}
File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME);
//加载缓存的dex文件(data/data/<packagename>/secondary-dexes)
List<File> files = MultiDexExtractor.load(context, applicationInfo, dexDir, false);
//检测文件是否是zip文件
if (checkValidZipFiles(files)) {
//加载缓存的dex
installSecondaryDexes(loader, dexDir, files);
} else {
//从Apk中提取dex
files = MultiDexExtractor.load(context, applicationInfo, dexDir, true);
if (checkValidZipFiles(files)) {
//加载提取的dex
installSecondaryDexes(loader, dexDir, files);
} else {
throw new RuntimeException("Zip files were not valid.");
}
}
}
} catch (Exception e) {
throw new RuntimeException("Multi dex installation failed (" + e.getMessage() + ").");
}
Log.i(TAG, "install done");
}

这里需要说明一下,在5.0以下的版本采用的是Dalvik,而5.0以上的运行时是ART。

具体代码的分析已经在上面代码的注释里给出了,从这里我们也可以看出,整个MultiDex.install(Context)的过程中,关键的步骤就是MultiDexExtractor#load方法和MultiDex#installSecondaryDexes方法。

先看MultiDexExtractor#load

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir,
boolean forceReload) throws IOException {
final File sourceApk = new File(applicationInfo.sourceDir);
//获取crc,为后面做验证用
long currentCrc = getZipCrc(sourceApk);
List<File> files;
//先判断是否强制重新解压,这里第一次会优先使用已解压过的dex文件,如果加载失败就强制重新解压。
if (!forceReload && !isModified(context, sourceApk, currentCrc)) {
try {
//加载缓存的dex
files = loadExistingExtractions(context, sourceApk, dexDir);
} catch (IOException ioe) {
//加载失败、重新解压
files = performExtractions(sourceApk, dexDir);
putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
}
} else {
//重新解压
files = performExtractions(sourceApk, dexDir);
putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
}
return files;
}

再看MultiDex#installSecondaryDexes

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
}
}
}

因为在不同的SDK版本上,ClassLoader加载dex文件的方式有所不同,所以这里做了V4/V14/V19的兼容。

这里主要看下 V14

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private static final class V14 {
private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
File optimizedDirectory)
throws IllegalArgumentException, IllegalAccessException,
NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
//通过反射获取loader的pathList字段,loader是由Application.getClassLoader()获取的,实际获取到的是PathClassLoader对象的pathList字段
final Field pathListField = findField(loader, "pathList");
final Object dexPathList = pathListField.get(loader);
//dexPathList是PathClassLoader的私有字段,里面保存的是Main Dex中的class
//dexElements是一个数组,里面的每一个item就是一个Dex文件
//为了把makeDexElements()返回的Elements[]对象添加到dexPathList字段的成员变量dexElements中
expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList<File>(additionalClassPathEntries), optimizedDirectory));
}
//返回的是其他Dex文件中获取到的Elements[]对象,内部通过反射makeDexElements()获取
private static Object[] makeDexElements(
Object dexPathList, ArrayList<File> files, File optimizedDirectory)
throws IllegalAccessException, InvocationTargetException,
NoSuchMethodException {
Method makeDexElements =
findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class);
return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory);
}
}

通过调用DexPathList#makeDexElements方法,可以加载我们上面解压得到的dex文件,我们看下V14的DexPathList源码(只看核心代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
final class DexPathList {
private static final String DEX_SUFFIX = ".dex";
private static final String JAR_SUFFIX = ".jar";
private static final String ZIP_SUFFIX = ".zip";
private static final String APK_SUFFIX = ".apk";
private static Element[] makeDexElements(ArrayList<File> files,
File optimizedDirectory) {
ArrayList<Element> elements = new ArrayList<Element>();
for (File file : files) {
ZipFile zip = null;
DexFile dex = null;
String name = file.getName();
if (name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ex) {
System.logE("Unable to load dex file: " + file, ex);
}
} else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
|| name.endsWith(ZIP_SUFFIX)) {
try {
zip = new ZipFile(file);
} catch (IOException ex) {
System.logE("Unable to open zip file: " + file, ex);
}
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ignored) {
}
} else {
System.logW("Unknown file type for: " + file);
}
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}
private static DexFile loadDexFile(File file, File optimizedDirectory)
throws IOException {
if (optimizedDirectory == null) {
return new DexFile(file);
} else {
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0);
}
}
/**
* Element of the dex/resource file path
*/
static class Element {
public final File file;
public final ZipFile zipFile;
public final DexFile dexFile;
public Element(File file, ZipFile zipFile, DexFile dexFile) {
this.file = file;
this.zipFile = zipFile;
this.dexFile = dexFile;
}
}
}

我们为什么要反射pathList字段呢?ClassLoader 在findClass()时会在pathList中寻找,因此通过反射手动添加其他Dex文件中的class到pathList字段中,就可以实现类的动态加载,这也是MultiDex方案的基本原理。

至此,MultiDex的源码分析到这里就结束了,其实有很多概念还没有讲清楚,只是讲了个大致思路,后面有时间再来补充吧

参考

其实你不知道MultiDex到底有多坑

MultiDex 工作原理分析和优化方案