因为单个DEX文件被限制在65535之内,在Android5.0以上的时候,Android系统开始支持多Dex,所以我们可以直接在build.gradle配置multiDexEnabled true就可以了,然后Android5.0以下仅仅支持单Dex,所以Google提供了相关的兼容库对Android5.0以下的系统进行兼容,这个兼容库就是Mutidex,接下我们将从如何兼容Dex讲起,然后了解相关的原理,最终提供启动时间的优化策略。
为了解决Android不支持多Dex的问题,Google官方引入了Mutidex,下面我们来了解一下Mutidex如何使用。
dependencies {
implementation 'com.android.support:multidex:1.0.1'
}
使用Mutidex的方法有两种方法:
因为JAVA支持的是单继承的关系,所以我们比较常用第二种方法。代码示例如下:
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
在进行Mutidex的源码分析前,我们应当有一个概念就是多Dex文件是如何命名的,所以我们通过AndroidStudio打开我们我APK文件,目录如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BhJuOBKY-1571450911640)(https://i.imgur.com/OtMYx7o.png)]
从图中我们可以看出,DEX文件的命名规范是classes.dex为主DEX文件,其余DEX为classes2.dex、classes3.dex、classes4.dex…以此类推为classes[序列号].dex。
不管是使用那种方法来实现Mutidex最终Mutidex都会调用MultiDex.install(this),接下来我们将从这个作为突破口,开始解析MultiDex的实现原理。
我们先来看MultiDex.install(this)的实现代码:
public static void install(Context context) {
if (IS_VM_MULTIDEX_CAPABLE) {
// 如何该系统支持多DEX文件运行 不做任何处理
Log.i("MultiDex", "VM has multidex support, MultiDex support library is disabled.");
} else if (VERSION.SDK_INT < 4) {
// 不做Android4以下的兼容 直接抛出异常
throw new RuntimeException("MultiDex installation failed. SDK " + VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + ".");
} else {
ApplicationInfo applicationInfo = getApplicationInfo(context);
if (applicationInfo == null) {
// 获取不到Application的信息,直接返回不做处理
Log.i("MultiDex", "No ApplicationInfo available, i.e. running on a test Context: MultiDex support library is disabled.");
return;
}
// 开始安装
doInstallation(context, new File(applicationInfo.sourceDir), new File(applicationInfo.dataDir), "secondary-dexes", "");
Log.i("MultiDex", "install done");
}
}
在这段代码中,我们总结以下逻辑就是:
下面我们来看看MultiDex.doInstallation()的实现:
// 已安装APK的列表
private static final Set installedApk = new HashSet();
private static void doInstallation(Context mainContext, File sourceApk,
File dataDir, String secondaryFolderName, String prefsKeyPrefix) throws ...{
// 以installedApk列表来同步
synchronized(installedApk) {
// 如果没有在安装列表里面的APK才进行安装
if (!installedApk.contains(sourceApk)) {
installedApk.add(sourceApk);
if (VERSION.SDK_INT > 20) {
// 如何Android5.0以上的系统不确保功能正常
}
// 获取程序的ClassLoader否则不做任何处理
ClassLoader loader;
try {
loader = mainContext.getClassLoader();
} catch (RuntimeException var11) {
return;
}
// 清理旧的Dex文件
clearOldDexDir(mainContext);
// 获取到Dex文件目录
File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);
// 提取到程序中所有的Dex压缩文件
List extends File> files = MultiDexExtractor.load(mainContext, sourceApk, dexDir, prefsKeyPrefix, false);
// 将Dex安装
nstallSecondaryDexes(loader, dexDir, files);
}
}
}
上面示例代码我删除了一些不是很重要异常和判断,以方便我们梳理逻辑,主要的流程为:
当我们跟踪到MultiDex.doInstallation()一条很清晰的流程就出来了,简单来说就是MultiDex从APK目录到抽取出所有的Classes.dex文件,然后将这些文件安装到程序,就能实现多DEX的运行了,为了更深入的理解,接下来我们将继续跟踪MultiDexExtractor.load方法。
static List extends File> load(Context context, File sourceApk, File dexDir, String prefsKeyPrefix, boolean forceReload) throws ... {
....
List files;
try {
// 文件加锁
if (!forceReload && !isModified(context, sourceApk, currentCrc, prefsKeyPrefix)) {
try {
// 加载缓存过的Classes.dex压缩文件
files = loadExistingExtractions(context, sourceApk, dexDir, prefsKeyPrefix);
} catch (IOException var21) {
// 加载已缓存的Classes.dex压缩文件失败,重新提取
files = performExtractions(sourceApk, dexDir);
// 保存在本地缓存
putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), currentCrc, files);
}
} else {
// 强制重新提取或者Classes.dex压缩文件被修改
files = performExtractions(sourceApk, dexDir);
// 保存在本地缓存
putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), currentCrc, files);
}
} finally {
// 解锁和关闭文件流
}
return files;
}
其中**MultiDexExtractor.performExtractions(sourceApk, dexDir)**是重点方法,我们看一下:
private static List performExtractions(File sourceApk, File dexDir) throws IOException {
...
// APK的压缩文件
ZipFile apk = new ZipFile(sourceApk);
try {
// 标记序列号
int secondaryNumber = 2;
// 遍历APK压缩文件里面的dex文件
for(ZipEntry dexFile = apk.getEntry("classes" + secondaryNumber + ".dex"); dexFile != null; dexFile = apk.getEntry("classes" + secondaryNumber + ".dex")) {
...
// 失败可以重试三次
while(numAttempts < 3 && !isExtractionSuccessful) {
++numAttempts;
// 压缩DEX文件
extract(apk, dexFile, extractedFile, extractedFilePrefix);
try {
extractedFile.crc = getZipCrc(extractedFile);
isExtractionSuccessful = true;
} catch (IOException var19) {
isExtractionSuccessful = false;
}
// 压缩失败删除临时文件
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 {
//关闭流
}
return files;
}
从代码中可以得出整个方法的核心就是对DEX文件进行压缩,然后进行返回,至此整个流程结束。
前面我们介绍了从APK压缩包中提取DEX压缩文件的流程,接下来我们可以看一下Mutidex是如何进行安装的。
private static void installSecondaryDexes(ClassLoader loader, File dexDir, List extends File> files) throws ... {
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);
}
}
}
以上代码是**Mutidex.installSecondaryDexes()方法中的源码,我们再看看MultiDex.V19.install()**里面的方法。
private static void install(ClassLoader loader, List extends File> additionalClassPathEntries, File optimizedDirectory) throws ... {
// 反射出ClassLoader的pathList字段
Field pathListField = MultiDex.findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
ArrayList suppressedExceptions = new ArrayList();
// 扩展数组并进行安装
MultiDex.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory, suppressedExceptions));
....
//异常处理
}
到了这一步我们就进行不下去了,需要了解ClassLoader的原理才能更加深入了解这些方法的作用,我这里直接说一下这些方法的功能:
在这里我们再总结一下整个MultiDex的流程:
以Android5.0源代码的为蓝本。
不管是 PathClassLoader还是DexClassLoader,都继承自BaseDexClassLoader,加载类的代码在BaseDexClassLoader中
public class BaseDexClassLoader extends ClassLoader {
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 suppressedExceptions = new ArrayList();
Class c = pathList.findClass(name, suppressedExceptions);
...
return c;
}
...
}
从BaseDexClassLoader.findClass()中可以看到,查找Class使用的是pathList的成员变量进行处理,而pathList是在BaseDexClassLoader的构造函数里面进行初始化的,其对象是DexPathList,那么接下来我们看看DexPathList.findClass()。
final class DexPathList {
private final Element[] dexElements;
public Class findClass(String name, List suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
}
从代码中可知在最终BaseDexClassLoader.findClass()是通过遍历DexPathList.dexElements数组的元素来查找Class.
下面是对加载Class过程的总结:
BaseDexClassLoader
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
Class clazz = pathList.findClass(name);
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
DexPathList
public Class findClass(String name) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext);
if (clazz != null) {
return clazz;
}
}
}
return null;
}
DexFile
public Class loadClassBinaryName(String name, ClassLoader loader) {
return defineClass(name, loader, mCookie);
}
private native static Class defineClass(String name, ClassLoader loader, int cookie);
可以看出,BaseDexClassLoader中有个pathList对象,pathList中包含一个DexFile的数组dexElements,由上面分析知道,dexPath传入的原始dex(.apk,.zip,.jar等)文件在optimizedDirectory文件夹中生成相应的优化后的odex文件,dexElements数组就是这些odex文件的集合,如果不分包一般这个数组只有一个Element元素,也就只有一个DexFile文件,而对于类加载呢,就是遍历这个集合,通过DexFile去寻找。最终调用native方法的defineClass。
学习过性能优化的同学应该都知道,SharedPreferences会导致UI暂停工作等待SharedPreferences完成后才进行,也就是说SharedPreferences保存数据如果使用SharedPreferences.Editor.commit()进行提交是同步行为,在我们学习Muldex原理的流程中缓存就是使用的SharedPreferences,所以Muldex在Android5.0以下会导致性能问题,如果我们需要让我们的APP更加稳定,那么我们就需要对其进行优化处理。
通过反编译今日头条APP的源码分析后发现,今日头条APP的优化思路是这样的:
基本的流程讲完,如果想要看代码样例可以打开:https://github.com/345509960/AndroidDemo/tree/master/MultiDexTest