Support-multidex原理

#简介:

之前介绍了Dex分包原理和65535原因,apk 分包官网给了详细的步骤。这里来说下分包过程的 compile 的 support-multidex 包的原理。

Support-multidex原理

首先集成 support-multidex,不多赘述。

分析的是 support-multidex-1.03 ,里面类不多,其实一共两个关键类:MultiDexMultiDexxtractor ,调用入口肯定是 MultiDex.installinstall 方法判断了是否符合 multidex 执行条件,如:虚拟机版本号和 SDK 版本号之类;其中会判断 Dalvik 虚拟机版本是否支持内置加载 Mulitdex 功能,如果虚拟机支持就用虚拟机内置的加载 Mulitdex 功能,然后 disable support-mulitdex library,如果虚拟机不支持 mulitdex 才会启用 support-multidex。

我写了个 Demo 看了下 MultiDex 的 log:

1
2
3
I/MultiDex: VM with version 2.1.0 has multidex support
I/MultiDex: Installing application
I/MultiDex: VM has multidex support, MultiDex support library is disabled.

虚拟机内置加载 multidex 功能应该和 support-multidex 大同小异,我们看下走 support-multidex 情况,判断完版本调用 doInstallation

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
/**
* @param sourceApk: /data/app/<pkgName>-1/base.apk
* @param dataDir : /data/user/0/<pkgName>
*/
private static void doInstallation(Context mainContext, File sourceApk, File dataDir, String secondaryFolderName, String prefsKeyPrefix, boolean reinstallOnPatchRecoverableException){
...
// 清理 /data/user/0/<pkgName>/files/secondary-dexes/ 目录下的 secondary dexes
// 之前版本 support-multidex 放 secondary dexes 的目录
clearOldDexDir(mainContext);
...
// secondary dexDir:/data/user/0/<pkgName>/code_cache/secondary-dexes
File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);
// secondary dexes 提取器
MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir);
...
try {
// 提取 secondary dexes 到指定目录
List files = extractor.load(mainContext, prefsKeyPrefix, false);
try {
// 加载 seondaryDexes
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);
// 第一次提取 secondary dexes 失败,尝试第二次强行提取
files = extractor.load(mainContext, prefsKeyPrefix, true);
// 加载 seondaryDexes
installSecondaryDexes(loader, dexDir, files);
}
...
}

主要逻辑就是:

  1. 检测是否需要 support-multidex 工作;
  2. 需要工作的话,先清理存储 secondary dexes 目录;
  3. 然后提取 apk secondary dexes 到 指定目录;
  4. 通过反射加载 secondary dexes

有两处关键逻辑:

  1. 提取 secondary dexes
  2. 加载 secondary dexes

先看提取 extractor.load

注:CRC的全称是循环冗余校验值:一种检测或者校验数据传输或者数据保存后可能出现错误的 哈希函数值。

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
/**
* 提取 secondary dexes 到 data 目录中的 files
*
* @param : forceReload : 第一次提取失败后,第二次强制提取的标识
*/
List<? extends File> load(Context context, String prefsKeyPrefix, boolean forceReload) throws IOException {
Log.i("MultiDex", "MultiDexExtractor.load(" + this.sourceApk.getPath() + ", " + forceReload + ", " + prefsKeyPrefix + ")");
// 保证提取操作同步
if(!this.cacheLock.isValid()) {
throw new IllegalStateException("MultiDexExtractor was closed");
} else {
List files;
// 通过 apk crc 校验值,检测 apk 如果改变了,就重新提取,否则直接用上次提取过的文件
if(!forceReload && !isModified(context, this.sourceApk, this.sourceCrc, prefsKeyPrefix)) {
try {
// 加载上次提取过的文件
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();
// 记录本次操作信息:apk 修改时间戳、apk crc 校验值、dex 数量、每个提取出来的 zip 的时间戳和crc
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.");
}
// 提取 seoncdary dexes 信息
files = this.performExtractions();
// 存储本次提取操作信息
putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files);
}
Log.i("MultiDex", "load found " + files.size() + " secondary dex files");
return files;
}
}

提取 secondary dexes 的操作:

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
private List<MultiDexExtractor.ExtractedDex> performExtractions() throws IOException {
// base.apk.classes
String extractedFilePrefix = this.sourceApk.getName() + ".classes";
// 清理 /data/user/0/<pkgName>/files/secondary-dexes/
// 这个目录就是这个版本 multidex 存放 secondary dexes 的
this.clearDexDir();
List<MultiDexExtractor.ExtractedDex> files = new ArrayList();
// 解压 apk
ZipFile apk = new ZipFile(this.sourceApk);
try {
// seoncdary dexes 从 2 开始 classes2.dex , classes3.dex ...
int secondaryNumber = 2;
for(ZipEntry dexFile = apk.getEntry("classes" + secondaryNumber + ".dex"); dexFile != null; dexFile = apk.getEntry("classes" + secondaryNumber + ".dex")) {
// fileName :base.apk.classes2.dex.zip
String fileName = extractedFilePrefix + secondaryNumber + ".zip";
// extractedFile :/data/user/0/<pkgName>/code_cache/secondary-dexes/base.apk/classes2.dex.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;
// 如果一直不成功,最多尝试 3 次提取 secondary dex
// 把 dexFile 中的字节写到 /data/user/0/<pkgName>/code_cache/secondary-dexes/base.apk/classesN.dex.zip 中
// 把 classesN.dex 字节一律转化为 classes.dex 一个 entry 压缩在 base.apk.classesN.dex.zip 中
while(numAttempts < 3 && !isExtractionSuccessful) {
++numAttempts;
extract(apk, dexFile, extractedFile, extractedFilePrefix);
try {
// 记录本次 file crc 校验值
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;
}

理一下提取 secondary dexes 的逻辑:

  1. 判断 apk 是否被修改,没有的话,加载之前提取的内容;
  2. performExtractions 解压 apk,把apk中的 classesN.dex (N>1) 字节提取到 /data/user/0//code_cache/secondary-dexes/base.apk.classesN.dex.zip 当中;
  3. putStoredApkInfo 保存本次提取操作信息,方便下次判断;

在看加载 secondary dexes 之前,我们来了解下 Android 中 classLoader 知识。

good good study

关于 ClassLoader 有两点要说:

  1. 类加载流程;
  2. 双亲委托模型;

类加载详细流程比较复杂,这里简单聊下。加载一个类到内存,首先会通过类的全限定名来获取此类的二进制字节流,但是虚拟机规范并没有规定怎么来获取二进制流,从哪里获取。这也是虚拟机设计团队有意为之,也因此诞生了很多强大的技术,比如:动态代理等。

所以获取二进制字节流的方式,既可以通过虚拟机的引导类加载器由虚拟机来完成加载,也可以通过我们自定义 ClassLoader 来完成。举个场景,比如,因为某些限制条件,需要把 Class 分开来加载,分开的部分可以用自定义的 ClassLoader 来加载到内存。听起来像不像 multiDex 中加载 Secondary dexes 哈哈。

也简单说下双亲委托模型,定义:除了顶层的 Bootstrap ClassLoader(所有 ClassLodaer 的父加载器) 外,所有 ClassLoader 都必须有父加载器;当收到类加载请求的时候,只有父加载器无法加载的时候,子加载器才会加载。

而标识一个 Class 需要这个类本身和加载该类的 ClassLoader 两者来决定。所以我们自定义的 java.lang.Object 和 JDK 中的 Object 不是同一个类,因为 JDK 中的类是通过虚拟机的顶层加载器 Bootstrap ClassLoader 加载的,而这个加载器只会加载特定文件,比如 JDK 中的 rt.jar,所以即便我们自己写个 java.lang.Object 打个 jar 包,丢进 JDK lib 目录也是不会被加载的。 所以在所有 ClassLoader 加载的 class 中 Object 类都是同一个。保证了 Java 程序安全性。

再来看 Android 中的 ClassLoader

1
2
3
System.out.println(context.getClassLoader.toString);
...
PathClassLoader[DexPathList[[zip file "/data/app/<pkgName>-1/base.apk"],nativeLibraryDirectories=[/data/app/<pkgName>-1/lib/arm64, /system/lib64, /vendor/lib64, /system/vendor/lib64, /product/lib64]]]

看来是 PathClassLoader 呢。

5.0 Android 源码 中 ClassLoader 的继承关系:

PathClassLoader 都继承自 BaseDexClassLoader

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
/**
* 1. 保存 dex/resource path
* 2. 保存 native code libraries path
*
* 保存的 entries 可能是 包含 classes.dex 的 jar 、 zip 或者是二进制 resources
* 亦或者是一个 .dex 文件
*/
final class DexPathList {
...
/**
* List of dex/resource (class path) elements.
*/
private final Element[] dexElements;
/** List of native library directories. */
private final File[] nativeLibraryDirectories;
...
/**
* 把 dex/resource list 中的资源处理并加载
* @param files: 保存 base.apk.classesN.dex.zip 的 list
*/
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
ArrayList<IOException> suppressedExceptions) {
ArrayList<Element> elements = new ArrayList<Element>();
/*
* 解压保存 classes.dex 的 zip
*/
for (File file : files) {
File zip = null;
DexFile dex = null;
String name = file.getName();
if (file.isDirectory()) {
// We support directories for looking up resources.
// This is only useful for running libcore tests.
elements.add(new Element(file, true, null, null));
} else if (file.isFile()){
if (name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
// 从字节实例化 DexFile
// 加载过程会判断 optimizedDirectory 是否为空
// 1. optimizedDirectory 为空,就直接 new DexFile 进行加载。
// 2. optimizedDirectory 不为空,进行 optimized(通过 native 方法进行 dex 合并)优化
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ex) {
System.logE("Unable to load dex file: " + file, ex);
}
...
return elements.toArray(new Element[elements.size()]);
}
/**
* 遍历 list 中的 dex 找到同名 class,如果该 class 还没有加载,就用这个 ClassLoader 的 context 来
* 加载 class.
*
* @param name of class to find
* @param suppressed exceptions encountered whilst finding the class
* @return the named class or {@code null} if the class is not
* found in any of the dex files
*/
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
// 最终通过 native 方法来加载 class
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
// 报 ClassNotFoundException 时候的输出
@Override public String toString() {
return "DexPathList[" + Arrays.toString(dexElements) +
",nativeLibraryDirectories=" + Arrays.toString(nativeLibraryDirectories) + "]";
}
...
}
public class BaseDexClassLoader extends ClassLoader {
// 存放 dex 文件和 so 文件的目录
private final DexPathList pathList;
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
// 交给 DexPathList 来加载 class
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
@Override public String toString() {
return getClass().getName() + "[" + pathList + "]";
}

主要类加载功能都在 BaseDexClassLoader 中了,但是他有两个子类 :PathClassLoaderDexClassLoader ,我们来看下这两者区别:

PathClassLoader :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package dalvik.system;
/**
* Provides a simple {@link ClassLoader} implementation that operates on a list
* of files and directories in the local file system, but does not attempt to
* load classes from the network. Android uses this class for its system class
* loader and for its application class loader(s).
*
* 能加载本地 class,但不能加载网络传输过来的 class。
* Android 用它加载系统 class 和 已经安装 apk 中的 class
* (之前我们在 app 中 System.out.println 过)
*/
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
}

DexClassLoader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package dalvik.system;
import java.io.File;
/**
* A class loader that loads classes from {@code .jar} and {@code .apk} files
* containing a {@code classes.dex} entry. This can be used to execute code not
* installed as part of an application.
*
* 从包含 classes.dex entry 的 jar 或者 apk 加载 class ;
* 可以执行并非来自 application 中的 code。这也是插件化和热修复的基础,在不需要安装应用的情况下,完成需要 使用的 dex 的加载。
*
* 使用 Context.getCodeCacheDir() 目录来缓存 optimized classes ,安全起见,不建议(只是不建议)使用外部存储来
* 存储 optimized classes。
*/
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
// 跟 PathClassLoader 不同的第二个参数;
// 会进行 DexOPT 优化,每次启动读取优化过的 dex 缓存
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}

可见DexClassLoaderPathClassLoader 只有调用父类构造器时候,是否传optimizedDirectory 的区别,跟踪BaseDexClassLoader 中的构造器optimizedDirectory引用:

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

optimizedDirectory 为空,会直接加载 Dex,而不为空的 loadDex 方法中有这么一段注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* ...
* This is intended for use by applications that wish to download
* and execute DEX files outside the usual application installation
* mechanism. This function should not be called directly by an
* application; instead, use a class loader such as
* dalvik.system.DexClassLoader.
* ...
* 用于当 application 希望下载并执行那些 来自非正常应用安装机制 的 DexFiles 时候。
* 这项功能不应该由 applicaton 来直接调用,而是自定义 DexClassLoader 来加载。
*/
static public DexFile loadDex(String sourcePathName, String outputPathName,
int flags) throws IOException {
return new DexFile(sourcePathName, outputPathName, flags);
}

所以 : PathClassLoader 用于加载系统 class 和已经安装的 apk; DexClassLoader 可以根据传入的 optimizedDirectory来加载自定义目录里的 dex、jar 等 class。

理一下 Android 的 class 加载过程 :BaseClassLoader 找到 makeDexElements 生成的 dexElements 然后遍历包含的 class 字节的 element, 最终通过 native 方法来加载所有 class 到内存。


再看加载 installSecondaryDexes :

1
2
3
4
5
6
7
8
9
10
11
private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<? extends File> files) {
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);
}
}
}

做了SDK Version 适配,原理一样,看一个分支即可,选择跟踪MultiDex.V19.install(loader, files, dexDir)

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
private static final class V19 {
private V19() {
}
/**
* @param loader : applicationContent.getClassLoader
* @param additionalClassPathEntries : secondary dexes 提取出来的 zip list
* @param optimizedDirectory : /data/user/0/<pkgName>/code_cache/secondary-dexes/
*/
static void install(ClassLoader loader, List<? extends File> additionalClassPathEntries, File optimizedDirectory) {
/*
* The patched class loader is expected to be a descendant of
* dalvik.system.BaseDexClassLoader. We modify its
* dalvik.system.DexPathList pathList field to append additional DEX
* file entries.
* 反射获取 BaseDexClassLoader 中的 pathList 把 secondary dexes 添加进去
*/
Field pathListField = MultiDex.findField(loader, "pathList");
Object dexPathList = pathListFiel d.get(loader);
...
// 通过反射用包含原始 dex 和 secondary dexes 的 element[] 替换原来的 dexlist 中的 element[]
MultiDex.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory, suppressedExceptions));
...
}
}

最后来理一下 Multidex 加载原理 :

  1. 判断 Davlik 版本,是否支持 内置 multidex 功能;和 SDK 版本是否支持 support-multidex;
  2. 实例化 ClassLoader ,确保 secondary dexes 目录存在并清理;
  3. MultidexExtractor 解压 apk 提取 classesN.dex 的字节到 /data/user/0//code_cache/secondary-dexes/base.apk/classesN.dex.zip 中;每个 zip 中都只有一个 名为 classes.dex 的 entry;
    1. 把当前 apk crc 校验值和 sharepreference 中之前保存的做比较,
    2. 如果相等,就直接用上次的提取过的 files;
    3. 如果不等,就重新执行提取操作,并保存本次 lastModified time 和 crc;
  4. 安装 secondary dexes :通过发射获取 BaseClassLoaderDexpathList 中的 Element[] 用包含 secondary dexes 的 新 Element[] 替换旧的。