一、为什么要进行Dex分包
Android下单个Dex文件存在65535函数数量的限制,而一个功能稍微复杂点的APP很容易超过这个限制,为此,我们引入了Dex分包,将一个App所有class分别打包为classes1.dex, classes2.dex, classes3.dex…
二、Dex加载时机
首先,从原理上分析,多个Dex的加载必须要在class被load进内存之前,否则会导致loadClass过程中出现ClassNotFoundException,而Dex的加载是通过MultiDex.install(context)方法进行的,这个方法的内部实现细节我们稍后分析。可以看到这个方法是需要context参数的,而在APP启动过程中,最早可以拿到context的回调就是Application.attachBaseContext(),对于App启动流程不清楚的可以点这里:App启动流程,所以Dex的加载我们会放在App的Application中的attachBaseContext()回调中进行:
public class MyApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
}
三、Dex加载原理
我们先来看看MultiDex.install()的源码
public static void install(Context context) {
Log.i(TAG, "install");
if (IS_VM_MULTIDEX_CAPABLE) {
Log.i(TAG, "VM has multidex support, MultiDex support library is disabled.");
return;
}
if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) {
throw new RuntimeException("Multi dex installation failed. SDK " + Build.VERSION.SDK_INT
+ " is unsupported. Min SDK version is " + MIN_SDK_VERSION + ".");
}
try {
ApplicationInfo applicationInfo = getApplicationInfo(context);
if (applicationInfo == null) {
return;
}
synchronized (installedApk) {
String apkPath = applicationInfo.sourceDir;
if (installedApk.contains(apkPath)) {
return;
}
installedApk.add(apkPath);
if (Build.VERSION.SDK_INT > MAX_SUPPORTED_SDK_VERSION) {
Log.w(TAG, "MultiDex is not guaranteed to work in SDK version "
+ Build.VERSION.SDK_INT + ": SDK version higher than "
+ MAX_SUPPORTED_SDK_VERSION + " 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") + "\"");
}
ClassLoader loader;
try {
loader = context.getClassLoader();
} catch (RuntimeException e) {
Log.w(TAG, "Failure while trying to obtain Context class loader. " +
"Must be running in test mode. Skip patching.", e);
return;
}
if (loader == null) {
Log.e(TAG,
"Context class loader is null. Must be running in test mode. "
+ "Skip patching.");
return;
}
try {
clearOldDexDir(context);
} catch (Throwable t) {
Log.w(TAG, "Something went wrong when trying to clear old MultiDex extraction, "
+ "continuing without cleaning.", t);
}
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.");
files = MultiDexExtractor.load(context, applicationInfo, dexDir, true);
if (checkValidZipFiles(files)) {
installSecondaryDexes(loader, dexDir, files);
} else {
throw new RuntimeException("Zip files were not valid.");
}
}
}
} catch (Exception e) {
Log.e(TAG, "Multidex installation failure", e);
throw new RuntimeException("Multi dex installation failed (" + e.getMessage() + ").");
}
Log.i(TAG, "install done");
}
直接看我这里标的1部分,前面的主要是一些抽取dex文件列表和校验的逻辑,下面是installSecondaryDexes(loader, dexDir, files)的源码
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);
}
}
}
开启套娃模式,再来看看V19.install的源码:
private static final class V19 {
private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
File optimizedDirectory)
throws IllegalArgumentException, IllegalAccessException,
NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
Field pathListField = findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
suppressedExceptions));
if (suppressedExceptions.size() > 0) {
for (IOException e : suppressedExceptions) {
Log.w(TAG, "Exception in makeDexElement", e);
}
Field suppressedExceptionsField =
findField(loader, "dexElementsSuppressedExceptions");
IOException[] dexElementsSuppressedExceptions =
(IOException[]) suppressedExceptionsField.get(loader);
if (dexElementsSuppressedExceptions == null) {
dexElementsSuppressedExceptions =
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(loader, dexElementsSuppressedExceptions);
}
}
private static Object[] makeDexElements(
Object dexPathList, ArrayList<File> files, File optimizedDirectory,
ArrayList<IOException> suppressedExceptions)
throws IllegalAccessException, InvocationTargetException,
NoSuchMethodException {
Method makeDexElements =
findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class,
ArrayList.class);
return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory,
suppressedExceptions);
}
}
四、MultiDex的一些问题与解决方案
1. MultiDex的性能问题
多dex在初始化时,通过刚才的代码分析我们知道,实际上在Application.attachBaseContext()中执行的MultiDex.install()方法内部会进行,包括dex读取、验证、合并、插入等一系列操作,首次启动还会做dex——>odex的转换,而默认情况下这些操作都是在UI线程进行的,且由于dex初始化非常靠前(实际上是在Activity初始化之前就完成了),所以如果分包过多实际上会被APP启动速度产生一定的负向影响。根据 Carlos Sessa的测试,启用multidex后,4.4或以下的设备,app的启动时间平均会增加15%,更严重的情况,甚至在启动时候会出现黑屏。
解决思路:
目前业界主流的解决思路主要有两种: ① 单独开启线程对MultiDex进行初始化,在通常在闪屏页开启,初始化完成后进去主页面; ② 单独开启进程并创建临时文件,初始化完成后删除临时文件并finish辅助进程,主进程会轮询临时文件是否存在,删除则进入主页面。(这个也是头条的方案,实际上头条还对MultiDex中多余的一次zip压缩进行了优化),详细方案可自行检索。
2. Multidex分包后主包方法数依旧超出65535问题
这种主要发生在一些大型APP,尤其是存在很多第三方依赖的APP,即时进行了初步分包,其主包方法数往往还是超过65535。
解决思路:
首先要明白分包的核心思路是主页面及其所依赖的类必须要在主包中,其他的可以放在子dex中启动后加载即可。使用SAX自行解析AndroidMainfest.xml,抽取出组件信息,将原始的Manifest_keep.txt内容替换掉,去除启动不需要的Activity组件,保证启动加载的类最小。 在gradle中添加multiDexExt扩展块,通过指定类名或通配符来设置必须编译在MainDex中类,在扩展块中指定的类都会被添加到maindexlist.txt文件汇中。详细可参考网易的优化思路:传送门
|