类加载机制系列3——MultiDex原理解析
1 MultiDex的由来
Android中由于一个dex文件最多存储65536个方法,也就是一个short类型的范围,所以随着应用的类不断增加,当一个dex文件突破这个方法数的时候就会报出异常。虽然可以通过混淆等方式来减少无用的方法,但是随着APP功能的增多,突破方法数限制还是不可避免的。因此在Android5.0时,Android推出了官方的解决方案:MultiDex。打包的时候,把一个应用分成多个dex,例如:classes.dex、classes2.dex、classes3.dex...,加载的时候把这些dex都追加到DexPathList对应的数组中,这样就解决了方法数的限制。
5.0后的系统都内置了加载多个dex文件的功能,而在5.0之前,系统只可以加载一个主dex,其它的dex就需要采用一定的手段来加载。这也就是我们今天要讲的MultiDex。
MultiDex存放在android.support.multidex包下
。
2 MultiDex的使用
Gradle构建环境下,在主应用的build.gradle文件夹添加如下配置:
defaultConfig {
...
multiDexEnabled true
...
}
dependencies {
compile 'com.android.support:multidex:1.0.1'
...
}
现在最新的multidex版本是1.0.2。
在AndroidManifest.xml中的app节点下,使用MultiDexApplication
作为应用入口。
package android.support.multidex;
...
public class MultiDexApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
}
当然了,大部分情况下,我们都会自定义一个自己的Application对应用做一些初始化。这种情况下,可以在我们自定义的Application中的attachBaseContext()方法中调用MultiDex.install()方法。
# 自定义的Applicaiton中
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
需要注意的是:MultiDex.install()方法的调用时机要尽可能的早,防止加载后面的dex文件中的类时报ClassNotFoundException。
3 MultiDex源码分析
分析MultiDex的的入口就是它的静态方法install()。
这个方法的作用就是把从应用的APK文件中的dex添加到应用的类加载器PathClassLoader中的DexPathList的Emlement数组中。
public static void install(Context context) {
Log.i(TAG, "install");
//判断Android系统是否已经支持了MultiDex,如果支持了就不需要再去安装了,直接返回
if (IS_VM_MULTIDEX_CAPABLE) {
Log.i(TAG, "VM has multidex support, MultiDex support library is disabled.");
return;
}
// 如果Android系统低于MultiDex最低支持的版本就抛出异常
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);
// 如果应用信息为空就返回,比如说运行在一个测试的Context下。
if (applicationInfo == null) {
// Looks like running on a test Context, so just return without patching.
return;
}
// 同步方法
synchronized (installedApk) {
// 获取已经安装的APK的全路径
String apkPath = applicationInfo.sourceDir;
if (installedApk.contains(apkPath)) {
return;
}
// 把路径添加到已经安装的APK路径中
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") + "\"");
}
/* 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.
*/
ClassLoader loader;
try {
// 获取ClassLoader,实际上是PathClassLoader
loader = context.getClassLoader();
} catch (RuntimeException e) {
/* Ignore those exceptions so that we don't break tests relying on Context like
* a android.test.mock.MockContext or a android.content.ContextWrapper with a
* null base Context.
*/
Log.w(TAG, "Failure while trying to obtain Context class loader. " +
"Must be running in test mode. Skip patching.", e);
return;
}
// 在某些测试环境下ClassLoader为null
if (loader == null) {
// Note, the context class loader is null when running Robolectric tests.
Log.e(TAG,
"Context class loader is null. Must be running in test mode. "
+ "Skip patching.");
return;
}
try {
// 清除老的缓存的Dex目录,来源的缓存目录是"/data/user/0/${packageName}/files/secondary-dexes"
clearOldDexDir(context);
} catch (Throwable t) {
Log.w(TAG, "Something went wrong when trying to clear old MultiDex extraction, "
+ "continuing without cleaning.", t);
}
// 新建一个存放dex的目录,路径是"/data/user/0/${packageName}/code_cache/secondary-dexes",用来存放优化后的dex文件
File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME);
// 使用MultiDexExtractor这个工具类把APK中的dex抽取到dexDir目录中,返回的files集合有可能为空,表示没有secondaryDex
// 不强制重新加载,也就是说如果已经抽取过了,可以直接从缓存目录中拿来使用,这么做速度比较快
List<File> files = MultiDexExtractor.load(context, applicationInfo, dexDir, false);
if (checkValidZipFiles(files)) {
// 如果抽取的文件是有效的,就安装secondaryDex
installSecondaryDexes(loader, dexDir, files);
} else {
Log.w(TAG, "Files were not valid zip files. Forcing a reload.");
// Try again, but this time force a reload of the zip file.
// 如果抽取出的文件是无效的,那么就强制重新加载,这么做的话速度就慢了一点,有一些IO开销
files = MultiDexExtractor.load(context, applicationInfo, dexDir, true);
if (checkValidZipFiles(files)) {
// 强制加载后,如果文件有效就安装,否则就抛出异常
installSecondaryDexes(loader, dexDir, files);
} else {
// Second time didn't work, give up
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");
}
关于dex文件抽取逻辑和校验逻辑我们先不管,我们看一下MultiDex是如何安装secondaryDex文件的。
由于不同版本的Android系统,类加载机制有一些不同,所以分为了V19、V14和V4等三种情况下的安装。V19、V14和V4都是MultiDex的private的静态内部类。V19支持Andorid19版本(20是只支持可穿戴设备的),V14支持14,、15、16、17 和 18版本,V4支持从4到13的版本。
# android.support.multidex.MultiDex
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的源码
/**
* Installer for platform versions 19.
*/
private static final class V19 {
private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
File optimizedDirectory)
throws IllegalArgumentException, IllegalAccessException,
NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
/* 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.
*/
// 传递的loader是PathClassLoader,findFidld()方法是遍历loader及其父类找到pathList字段
// 实际上就是找到BaseClassLoader中的DexPathList
Field pathListField = findField(loader, "pathList");
// 获取PathClassLoader绑定的DexPathList对象
Object dexPathList = pathListField.get(loader);
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
// 扩展DexPathList对象的Element数组,数组名是dexElements
// makeDexElements()方法的作用就是调用DexPathList的makeDexElements()方法来创建dex元素
expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
suppressedExceptions));
// 后面就是添加一些IO异常信息,因为调用DexPathList的makeDexElements会有一些IO操作,相应的可能就会有一些异常情况
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);
}
}
/**
* A wrapper around
* {@code private static final dalvik.system.DexPathList#makeDexElements}.
*/
// 通过反射的方式调用DexPathList#makeDexElements()方法
// dexPathList 就是一个DexPathList对象
private static Object[] makeDexElements(
Object dexPathList, ArrayList<File> files, File optimizedDirectory,
ArrayList<IOException> suppressedExceptions)
throws IllegalAccessException, InvocationTargetException,
NoSuchMethodException {
// 获取DexPathList的makeDexElements()方法
Method makeDexElements =
findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class,
ArrayList.class);
// 调用makeDexElements()方法,根据外界传递的包含dex文件的源文件和优化后的缓存目录返回一个Element[]数组
return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory,
suppressedExceptions);
}
}
MultiDex的expandFieldArray()方法作用是扩展一个对象中的数组中的元素。实际上就是一个工具方法。简单看一下源码:
# android.support.multidex.MultiDex
private static void expandFieldArray(Object instance, String fieldName,
Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException,
IllegalAccessException {
Field jlrField = findField(instance, fieldName);
Object[] original = (Object[]) jlrField.get(instance);
Object[] combined = (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);
}
V19的install()方法调用完毕之后,就把APK文件中的主dex文件之外的dex文件追加到PathClassLoader(也就是BaseClassLoader)中DexPathListde Element[]数组中。这样在加载一个类的时候就会遍历所有的dex文件,保证了打包的类都能够正常加载。
至于V14和V4中的install()方法,主要的思想都是一致的,在细节上有一些不同,有兴趣的可以自行查看相关源码。
小结一下:
MultiDex的install()方法实际上是先抽取出APK文件中的.dex文件,然后利用反射把这个.dex文件生成对应的数组,最后把这些dex路径追加到PathClassLoader加载dex的路径中,从而保证了APK中所有.dex文件中类都能够被正确的加载。
分析完了,MultiDex加载secondartDex的逻辑,我们再来看一下从APK文件中抽取出.dex文件的逻辑。
看一下MultiDexExtractor的load()方法:
# android.support.multidex.MultiDexExtractor
static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir,
boolean forceReload) throws IOException {
Log.i(TAG, "MultiDexExtractor.load(" + applicationInfo.sourceDir + ", " + forceReload + ")");
// sourceDir 路径为"/data/app/}${packageName}-1/base.apk"
final File sourceApk = new File(applicationInfo.sourceDir);
// 获取APK文件的CRC(循环冗余校验)
long currentCrc = getZipCrc(sourceApk);
List<File> files;
// 如果不需要重新加载并且文件没有被修改过
// isModified()方法是根据SharedPreference中存放的APK文件上一次修改的时间戳和currentCrc来判断是否修改过文件
if (!forceReload && !isModified(context, sourceApk, currentCrc)) {
try {
// 从缓存目录中加载已经抽取过的文件
files = loadExistingExtractions(context, sourceApk, dexDir);
} catch (IOException ioe) {
Log.w(TAG, "Failed to reload existing extracted secondary dex files,"
+ " falling back to fresh extraction", ioe);
// 如果从缓存中加载失败就需要冲APK文件中去加载,这个过程时间会长一点
files = performExtractions(sourceApk, dexDir);
// 把抽取信息保存到SharedPreferences中,方便下次使用
putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
}
} else {
// 如果强制加载或者APK文件已经修改过就重新抽取dex文件
Log.i(TAG, "Detected that extraction must be performed.");
files = performExtractions(sourceApk, dexDir);
putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
}
Log.i(TAG, "load found " + files.size() + " secondary dex files");
return files;
}
根据前后顺序的话,App第一次运行的时候需要从APK冲抽取dex文件,我们先来看一下MultiDexExtractor的performExtractions()方法:
# android.support.multidex.MultiDexExtractor
private static List<File> performExtractions(File sourceApk, File dexDir)
throws IOException {
// 抽取出的dex文件名前缀是"${apkName}.classes"
final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
// Ensure that whatever deletions happen in prepareDexDir only happen if the zip that
// contains a secondary dex file in there is not consistent with the latest apk. Otherwise,
// multi-process race conditions can cause a crash loop where one process deletes the zip
// while another had created it.
// 由于这个dexDir缓存目录可能不止一个APK在使用,在抽取一个APK之前如果有缓存过的与APK相关的dex文件就需要先删除掉,如果dexDir目录不存在就需要创建
prepareDexDir(dexDir, extractedFilePrefix);
List<File> files = new ArrayList<File>();
final ZipFile apk = new ZipFile(sourceApk);
try {
int secondaryNumber = 2;
// 获取"classes${secondaryNumber}.dex"格式的文件
ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
// 如果dexFile不为null就一直遍历
while (dexFile != null) {
// 抽取后的文件名是"${apkName}.classes${secondaryNumber}.zip"
String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
// 创建文件
File extractedFile = new File(dexDir, fileName);
// 添加到集合中
files.add(extractedFile);
Log.i(TAG, "Extraction is needed for file " + extractedFile);
// 抽取过程中存在失败的可能,可以多次尝试,使用isExtractionSuccessful作为是否成功的标志
int numAttempts = 0;
boolean isExtractionSuccessful = false;
while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) {
numAttempts++;
// Create a zip file (extractedFile) containing only the secondary dex file
// (dexFile) from the apk.
// 抽出去apk中对应序号的dex文件,存放到extractedFile这个zip文件中,只包含它一个
// extract方法就是一个IO操作
extract(apk, dexFile, extractedFile, extractedFilePrefix);
// Verify that the extracted file is indeed a zip file.
// 判断是够抽取成功
isExtractionSuccessful = verifyZipFile(extractedFile);
// Log the sha1 of the extracted zip file
Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "success" : "failed") +
" - length " + extractedFile.getAbsolutePath() + ": " +
extractedFile.length());
if (!isExtractionSuccessful) {
// Delete the extracted file
extractedFile.delete();
if (extractedFile.exists()) {
Log.w(TAG, "Failed to delete corrupted secondary dex '" +
extractedFile.getPath() + "'");
}
}
}
if (!isExtractionSuccessful) {
throw new IOException("Could not create zip file " +
extractedFile.getAbsolutePath() + " for secondary dex (" +
secondaryNumber + ")");
}
// 继续下一个dex的抽取
secondaryNumber++;
dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
}
} finally {
try {
apk.close();
} catch (IOException e) {
Log.w(TAG, "Failed to close resource", e);
}
}
return files;
}
当MultiDexExtractor的performExtractions()方法调用完毕的时候就把APK中所有的dex文件抽取出来,并以一定文件名格式的zip文件保存在缓存目录中。然后再把一些关键的信息通过调用putStoredApkInfo(Context context, long timeStamp, long crc, int totalDexNumber)
方法保存到SP中。
当APK之后再启动的时候就会从缓存目录中去加载已经抽取过的dex文件。我们接着来看一下MultiDexExtractor的loadExistingExtractions()方法:
# android.support.multidex.MultiDexExtractor
private static List<File> loadExistingExtractions(Context context, File sourceApk, File dexDir)
throws IOException {
Log.i(TAG, "loading existing secondary dex files");
// 抽取出的dex文件名前缀是"${apkName}.classes"
final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
// 从SharedPreferences中获取.dex文件的总数量,调用这个方法的前提是已经抽取过dex文件,所以SP中是有值的
int totalDexNumber = getMultiDexPreferences(context).getInt(KEY_DEX_NUMBER, 1);
final List<File> files = new ArrayList<File>(totalDexNumber);
// 从第2个dex开始遍历,这是因为主dex由Android系统自动加载的,从第2个开始即可
for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) {
// 文件名,格式是"${apkName}.classes${secondaryNumber}.zip"
String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
// 根据缓存目录和文件名得到抽取后的文件
File extractedFile = new File(dexDir, fileName);
// 如果是一个文件就保存到抽取出的文件列表中
if (extractedFile.isFile()) {
files.add(extractedFile);
if (!verifyZipFile(extractedFile)) {
Log.i(TAG, "Invalid zip file: " + extractedFile);
throw new IOException("Invalid ZIP file.");
}
} else {
throw new IOException("Missing extracted secondary dex file '" +
extractedFile.getPath() + "'");
}
}
return files;
}
4 总结
分析到这,MultiDex安装多个dex的原理应该介绍清楚了,无非就是通过一定的方式把dex文件抽取出来,然后把这些dex文件追加到DexPathList的Element[]数组的后面,这个过程要尽可能的早,所以一般是在Application的attachBaseContext()方法中。
一些热修复技术,就是通过一定的方式把修复后的dex插入到DexPathList的Element[]数组前面,实现了修复后的class抢先加载。