工作流程
MultiDex的工作流程具体分为两个部分,一个部分是打包构建Apk的时候,将Dex文件拆分成若干个小的Dex文件,这个Android Studio已经帮我们做了(设置 “multiDexEnabled true”),另一部分就是在启动Apk的时候,同时加载多个Dex文件(具体是加载Dex文件优化后的Odex文件,不过文件名还是.dex),这一部分工作从Android 5.0开始系统已经帮我们做了,但是在Android 5.0以前还是需要通过MultiDex Support库来支持(MultiDex.install(Context))。
所以我们需要关心的是第二部分,这个过程的简单示意流程图如下。
MultiDex
MultiDex的入口是MultiDex.install(Context)
install()
private static final int MIN_SDK_VERSION = 4;
private static final boolean IS_VM_MULTIDEX_CAPABLE =
isVMMultidexCapable(System.getProperty("java.vm.version"));
public static void install(Context context) {
Log.i(TAG, "Installing application");
//判断是否需要执行MULTIDEX
if (IS_VM_MULTIDEX_CAPABLE) {
Log.i(TAG, "VM has multidex support, MultiDex support library is disabled.");
return;
}
//sdk<4 抛出异常
if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) {
throw new RuntimeException("MultiDex installation failed. SDK " + Build.VERSION.SDK_INT
+ " is unsupported. Min SDK version is " + MIN_SDK_VERSION + ".");
}
try {
//获取ApplicationInfo
ApplicationInfo applicationInfo = getApplicationInfo(context);
if (applicationInfo == null) {
Log.i(TAG, "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),
CODE_CACHE_SECONDARY_FOLDER_NAME,
NO_KEY_PREFIX,
true);
} catch (Exception e) {
Log.e(TAG, "MultiDex installation failure", e);
throw new RuntimeException("MultiDex installation failed (" + e.getMessage() + ").");
}
Log.i(TAG, "install done");
}
doInstallation()
private static void doInstallation(Context mainContext, File sourceApk, File dataDir,
String secondaryFolderName, String prefsKeyPrefix,
boolean reinstallOnPatchRecoverableException) throws IOExc
IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
InvocationTargetException, NoSuchMethodException, SecurityException,
ClassNotFoundException, InstantiationException {
synchronized (installedApk) {
if (installedApk.contains(sourceApk)) {
return;
}
installedApk.add(sourceApk);
//如果当前Android版本已经自身支持了MultiDex,依然可以执行MultiDex操作,但是会输出警告日志。
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 ClassLoader capable of loading DEX
* bytecode. We modify its pathList field to append additional DEX file entries.
*/
//获取当前的ClassLoader实例,后面要做的工作,就是把其他dex文件加载后
//把其DexFile对象添加到这个ClassLoader实例里就完事了。
ClassLoader loader = getDexClassloader(mainContext);
if (loader == null) {
return;
}
try {
// 清除旧的dex文件,注意这里不是清除上次加载的dex文件缓存。
// 获取dex缓存目录是,会优先获取/data/data/<package>/code-cache作为缓存目录。
// 如果获取失败,则使用/data/data/<package>/files/code-cache目录。
// 这里清除的是第二个目录。
clearOldDexDir(mainContext);
} catch (Throwable t) {
Log.w(TAG, "Something went wrong when trying to clear old MultiDex extraction, "
+ "continuing without cleaning.", t);
}
//获取缓存目录(/data/data/<package>/code-cache)。
File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);
// MultiDexExtractor is taking the file lock and keeping it until it is closed.
// Keep it open during installSecondaryDexes and through forced extraction to ensure
// extraction or optimizing dexopt is running in parallel.
//创建MultiDexExtractor
MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir);
IOException closeException = null;
try {
//加载文件
List<? extends File> files =
extractor.load(mainContext, prefsKeyPrefix, false);
try {
//安装dex
installSecondaryDexes(loader, dexDir, files);
// Some IOException causes may be fixed by a clean extraction.
} catch (IOException e) {
if (!reinstallOnPatchRecoverableException) {
throw e;
}
Log.w(TAG, "Failed to install extracted secondary dex files, retrying with "
+ "forced extraction", e);
files = extractor.load(mainContext, prefsKeyPrefix, true);
installSecondaryDexes(loader, dexDir, files);
}
} finally {
try {
extractor.close();
} catch (IOException e) {
// Delay throw of close exception to ensure we don't override some exception
// thrown during the try block.
closeException = e;
}
}
if (closeException != null) {
throw closeException;
}
}
}
具体代码的分析已经在上面代码的注释里给出了,从这里我们也可以看出,整个MultiDex.install(Context)的过程中,关键的步骤就是MultiDexExtractor#load方法和MultiDex#installSecondaryDexes方法。
MultiDexExtractor
MultiDexExtractor的构造函数
MultiDexExtractor(File sourceApk, File dexDir) throws IOException {
Log.i(TAG, "MultiDexExtractor(" + sourceApk.getPath() + ", " + dexDir.getPath() + ")");
this.sourceApk = sourceApk;
this.dexDir = dexDir;
//获取当前apk文件的crc值
sourceCrc = getZipCrc(sourceApk);
File lockFile = new File(dexDir, LOCK_FILENAME);
lockRaf = new RandomAccessFile(lockFile, "rw");
try {
lockChannel = lockRaf.getChannel();
try {
Log.i(TAG, "Blocking on lock " + lockFile.getPath());
//加上文件锁,防止多进程冲突
cacheLock = lockChannel.lock();
} catch (IOException | RuntimeException | Error e) {
closeQuietly(lockChannel);
throw e;
}
Log.i(TAG, lockFile.getPath() + " locked");
} catch (IOException | RuntimeException | Error e) {
closeQuietly(lockRaf);
throw e;
}
}
load()
List<? extends File> load(Context context, String prefsKeyPrefix, boolean forceReload)
throws IOException {
Log.i(TAG, "MultiDexExtractor.load(" + sourceApk.getPath() + ", " + forceReload + ", " +
prefsKeyPrefix + ")");
if (!cacheLock.isValid()) {
throw new IllegalStateException("MultiDexExtractor was closed");
}
List<ExtractedDex> files;
// 先判断是否强制重新解压,这里第一次会优先使用已解压过的dex文件,如果加载失败就强制重新解压。
// 此外,通过crc和文件修改时间,判断如果Apk文件已经被修改(覆盖安装),就会跳过缓存重新解压dex文件。
if (!forceReload && !isModified(context, sourceApk, sourceCrc, prefsKeyPrefix)) {
try { //加载缓存的dex文件
files = loadExistingExtractions(context, prefsKeyPrefix);
} catch (IOException ioe) {
Log.w(TAG, "Failed to reload existing extracted secondary dex files,"
+ " falling back to fresh extraction", ioe);
//加载失败的话重新解压,并保存解压出来的dex文件的信息。
files = performExtractions();
putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), sourceCrc,
files);
}
} else {
if (forceReload) {
Log.i(TAG, "Forced extraction must be performed.");
} else {
Log.i(TAG, "Detected that extraction must be performed.");
}
//重新解压,并保存解压出来的dex文件的信息。
files = performExtractions();
putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), sourceCrc,
files);
}
Log.i(TAG, "load found " + files.size() + " secondary dex files");
return files;
}
performExtractions()
performExtractions方法负责抽取dex
private List<ExtractedDex> performExtractions() throws IOException {
//文件前缀
final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
// It is safe to fully clear the dex dir because we own the file lock so no other process is
// extracting or running optimizing dexopt. It may cause crash of already running
// applications if for whatever reason we end up extracting again over a valid extraction.
clearDexDir();
List<ExtractedDex> files = new ArrayList<ExtractedDex>();
final ZipFile apk = new ZipFile(sourceApk);
try {
int secondaryNumber = 2;
ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
while (dexFile != null) {
String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
ExtractedDex extractedFile = new ExtractedDex(dexDir, fileName);
files.add(extractedFile);
Log.i(TAG, "Extraction is needed for file " + extractedFile);
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.
extract(apk, dexFile, extractedFile, extractedFilePrefix);
// Read zip crc of extracted dex
try {
extractedFile.crc = getZipCrc(extractedFile);
isExtractionSuccessful = true;
} catch (IOException e) {
isExtractionSuccessful = false;
Log.w(TAG, "Failed to read crc from " + extractedFile.getAbsolutePath(), e);
}
// Log size and crc of the extracted zip file
Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "succeeded" : "failed")
+ " '" + extractedFile.getAbsolutePath() + "': length "
+ extractedFile.length() + " - crc: " + extractedFile.crc);
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 + ")");
}
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;
}
这个过程主要是获取可以安装的dex文件列表,可以是上次解压出来的缓存文件,也可以是重新从Apk包里面提取出来的。需要注意的时,如果是重新解压,这里会有明显的耗时,而且解压出来的dex文件,会被压缩成.zip压缩包,压缩的过程也会有明显的耗时(这里压缩dex文件可能是问了节省空间)。
如果dex文件是重新解压出来的,则会保存dex文件的信息,包括解压的apk文件的crc值、修改时间以及dex文件的数目,以便下一次启动直接使用已经解压过的dex缓存文件,而不是每一次都重新解压。
无论是通过使用缓存的dex文件,还是重新从apk中解压dex文件,获取dex文件列表后,下一步就是安装(或者说加载)这些dex文件了。最后的工作在MultiDex#installSecondaryDexes这个方法里面。
Dex安装
installSecondaryDexes()
因为在不同的SDK版本上,ClassLoader(更准确来说是DexClassLoader)加载dex文件的方式有所不同,所以这里做了V4/V14/V19的兼容(Magic Code)。
private static void installSecondaryDexes(ClassLoader loader, File dexDir,
List<? extends File> files)
throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
InvocationTargetException, NoSuchMethodException, IOException, SecurityException,
ClassNotFoundException, InstantiationException {
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);
} else {
V4.install(loader, files);
}
}
}
V19与V14差别不大,只不过DexPathList#makeDexElements方法多了一个ArrayList参数,如果在执行DexPathList#makeDexElements方法的过程中出现异常,后面使用反射的方式把这些异常记录进DexPathList的dexElementsSuppressedExceptions字段里面。这里我们分析下V19的install方法。
static void install(ClassLoader loader,
List<? extends File> additionalClassPathEntries,
File optimizedDirectory)
throws IllegalArgumentException, IllegalAccessException,
NoSuchFieldException, InvocationTargetException, NoSuchMethodException,
IOException {
/* 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的pathList字段
Field pathListField = findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
//makeDexElements方法反射获取DexPathList#makeDexElement
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(dexPathList, "dexElementsSuppressedExceptions");
IOException[] dexElementsSuppressedExceptions =
(IOException[]) suppressedExceptionsField.get(dexPathList);
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(dexPathList, dexElementsSuppressedExceptions);
IOException exception = new IOException("I/O exception during makeDexElement");
exception.initCause(suppressedExceptions.get(0));
throw exception;
}
}
makeDexElements()
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);
}
expandFieldArray()
expandFieldArray负责为dexElements设置新的数组
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);
}
无论是V4/V14还是V19,在创建DexFile对象的时候,都需要通过DexFile的Native方法openDexFile来打开dex文件,其具体细节暂不讨论(涉及到dex的文件结构,很烦,有兴趣请阅读dalvik_system_DexFile.cpp),这个过程的主要目的是给当前的dex文件做Optimize优化处理并生成相同文件名的odex文件,App实际加载类的时候,都是通过odex文件进行的。因为每个设备对odex格式的要求都不一样,所以这个优化的操作只能放在安装Apk的时候处理,主dex的优化我们已经在安装apk的时候搞定了,其余的dex就是在MultiDex#installSecondaryDexes里面优化的,而后者也是MultiDex过程中,另外一个耗时比较多的操作。(在MultiDex中,提取出来的dex文件被压缩成.zip文件,又优化后的odex文件则被保存为.dex文件。)