源码分析-multiDex

概述

MultiDex适用于API版本在4-20的Android系统 , 即Android 2.1 - 4.4 . 而在这些版本之间 , MultiDex会通过Application.getClassLoader进行加载. 而如果Dex比较多比较大的话 , 主线程加载Dex时间会很长 , 导致主线程ANR.

由于Android 5.0之后使用ART虚拟机进行dex2oat , 将多dex在安装的时候将APK中多个Dex进行优化 , 优化过后生成一个ELF文件 , 名为.oat文件. 在加载后 , 会将oat文件直接映射到ART虚拟机中使用 , 这样就减少Dex加载的耗时..

MultiDex加载过程简述

  • 读取APK的CRC32以及modifyTime进行校验
  • 通过反射 , 从BaseDexClassLoader中找到pathList对象
  • 通过反射调用PathList.makeDexElements创建Elements[]
  • 通过反射将Elements[]添加到dexElements数组中
  • 后续在该ClassLoader查找类到时候 , 会优先在dexElements中开始遍历查找

MultiDex加载过程

MultiDex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void install(Context context) {
if (IS_VM_MULTIDEX_CAPABLE) {
//VM版本大于2.1时,IS_VM_MULTIDEX_CAPABLE为true,这时候MultiDex.install什么也不用做,直接返回。因为大于2.1的VM会在安装应用的时候,就把多个dex合并到一块
} else if (VERSION.SDK_INT < 4) {
//Multi dex最小支持的SDK版本为4
throw new RuntimeException("Multi dex installation failed. SDK " + VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + ".");
} else {
// 这里是重点逻辑
File dexDir = new File(e.dataDir, "code_cache/secondary-dexes");
//1. 把dex文件缓存到/data/data/<packagename>/code_cache/secondary-dexes/目录
List files = MultiDexExtractor.load(context, e, dexDir, false);
//2. 安装所有的附属dex
installSecondaryDexes(loader, dexDir, files);
}
}

执行文件拷贝工作

1
2
3
4
5
6
7
8
9
MultiDexExtractor.load(){
try {
//从缓存目录中直接查找缓存文件,跳过解压
files = loadExistingExtractions(context, sourceApk, dexDir);
} catch (IOException var9) {
files = performExtractions(sourceApk, dexDir);
putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
}
}

获取到所有的dex文件

1
2
3
4
5
6
7
8
9
MultiDexExtractor.performExtractions() {
for (ZipEntry dexFile = apk.getEntry("classes" + e + ".dex"); dexFile != null; dexFile = apk.getEntry("classes" + e + ".dex")) {
String fileName = extractedFilePrefix + e + ".zip";
File extractedFile = new File(dexDir, fileName);
files.add(extractedFile);
}

}

1
2
3
4
5
6
7
8
9
10
11
MultiDex.installSecondaryDexes() {
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, dexDir);
} else {
MultiDex.V4.install(loader, files);
}
}
}
1
2
3
4
5
6
7
MultiDex.V14.install(loader, files, dexDir){

Field pathListField = MultiDex.findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
MultiDex.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory));

}
1
2
3
4
5
6
7
private static Object[] makeDexElements(Object dexPathList,
ArrayList<File> files,
File optimizedDirectory) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
Method makeDexElements = MultiDex.findMethod(dexPathList, "makeDexElements", new Class[]{ArrayList.class, File.class});
return (Object[]) ((Object[]) makeDexElements.invoke(dexPathList, new Object[]{files, optimizedDirectory}));
}

1
2
3
4
5
6
7
8
9
10
11
private static void expandFieldArray(Object instance,
String fieldName,
Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
Field jlrField = findField(instance, fieldName);
Object[] original = (Object[]) ((Object[]) jlrField.get(instance));
Object[] combined = (Object[]) ((Object[]) Array.newInstance(original.getClass()
.getComponentType(), original.length + extraElements.length));
System.arraycopy(original, 0, combined, 0, original.length);
System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
jlrField.set(instance, combined);
}

Multidex的缺陷

在冷启动时因为需要安装DEX文件,如果DEX文件过大时,处理时间过长,很容易引发ANR

解决方案

  1. 第一次启动的时候,检测到未曾加载过second dex,那么启动欢迎页面(启动新的进程,原来进程进入阻塞等待,注意,此时不会发生ANR,因为已经不是前台进程了),在欢迎页面里面进行second dex的加载,加载完成后通知主线程继续
  2. 设定单个dex文件最大方法数为48000(经验值)而不是65536,避免内存问题
  3. 控制程序逻辑,未曾加载完second dex之前,进入阻塞等待,直到加载完程序才往下走
1
2
3
Application.attachBaseContext(Context context) {
loadMultiDex(context)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void loadMultiDex(Context context) {
newTempFile(context); //创建临时文件

//启动另一个进程去加载MultiDex
Intent intent = new Intent(context, LoadMultiDexActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);

//死循环 ,检查MultiDex是否安装完(安装完会删除临时文件)
checkUntilLoadDexSuccess(context);

//另一个进程以及加载 MultiDex,有缓存了,所以主进程再加载就很快了。
//为什么主进程要再加载,因为每个进程都有一个ClassLoader

MultiDex.install(context);

}
1
2
3
4
LoadMultiDexActivity.onCreate() {
new thread().run{loadMultiDex();}

}
1
2
3
4
5
6
7
8
9
10
11
12
loadMultiDex(){
//执行加载
MultiDex.install(LoadMultiDexActivity.this);
//加载结束后,删除临时文件,application就可以继续执行了
deleteTempFile(this);

//将这个进程杀死
Log.d(TAG, "aftetMultiDex: ");
finish();
Process.killProcess(Process.myPid());
}

抖音BoostMultiDex优化实践