工作流程
MultiDex的工作流程具体分为两个部分,一个部分是打包构建Apk的时候,将Dex文件拆分成若干个小的Dex文件,这个Android Studio已经帮我们做了(设置 “multiDexEnabled true”),另一部分就是在启动Apk的时候,同时加载多个Dex文件(具体是加载Dex文件优化后的Odex文件,不过文件名还是.dex),这一部分工作从Android 5.0开始系统已经帮我们做了,但是在Android 5.0以前还是需要通过MultiDex Support库来支持(MultiDex.install(Context))。
所以这篇文章主要分析第二部分加载多个Dex文件
源码分析
MultiDex Support的入口是MultiDex.install(Context)
MultiDex.install(Context)
public static void install(Context context) {
Log.i(TAG, "Installing application");
// 1. 判读是否需要执行MultiDex。
if (IS_VM_MULTIDEX_CAPABLE) {
Log.i(TAG, "VM has multidex support, MultiDex support library is disabled.");
return;
}
//private static final int MIN_SDK_VERSION = 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 = 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");
}
获取ApplicationInfo
,执行doInstallation
方法。
MultiDex.doInstallation
private static void doInstallation(Context mainContext, File sourceApk, File dataDir,
String secondaryFolderName, String prefsKeyPrefix,
boolean reinstallOnPatchRecoverableException) throws IOException,
IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
InvocationTargetException, NoSuchMethodException, SecurityException,
ClassNotFoundException, InstantiationException {
//如果这个方法已经调用过一次,就不能再调用了
synchronized (installedApk) {
if (installedApk.contains(sourceApk)) {
return;
}
installedApk.add(sourceApk);
//private static final int MAX_SUPPORTED_SDK_VERSION = 20;
//如果当前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") + "\"");
}
//1 返回一个能够读取字节码的ClassLoader,修改他的pathList成员增加DexFile
ClassLoader loader = getDexClassloader(mainContext);
if (loader == null) {
return;
}
try {
//2清除缓存目录/data/data/<package>/files/code-cache/secondary-dexes目录
clearOldDexDir(mainContext);
} catch (Throwable t) {
Log.w(TAG, "Something went wrong when trying to clear old MultiDex extraction, "
+ "continuing without cleaning.", t);
}
//3获取dex缓存目录,具体看下面分析/data/data/<package>/code_cache/secondary-dexes
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 no
// extraction or optimizing dexopt is running in parallel.
//4 通过MultiDexExtractor加载缓存文件
MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir);
IOException closeException = null;
try {
List<? extends File> files =
extractor.load(mainContext, prefsKeyPrefix, false);
try {
//5安装提取的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;
}
}
}
在注释1处返回一个能够读取字节码的ClassLoader,在注释2处清理/data/data/<package>/code-cache
的dex缓存目录,在注释3处获取dex缓存目录。
MultiDex.getDexDir
MultiDex在获取dex缓存目录是,会优先获取/data/data//code-cache作为缓存目录,如果获取失败,则使用/data/data//files/code-cache目录,而后者的缓存文件会在每次App重新启动的时候被清除。
private static File getDexDir(Context context, File dataDir, String secondaryFolderName)
throws IOException {
// private static final String CODE_CACHE_NAME = "code_cache";
//优先获取/data/data/<package>/code-cache作为缓存目录。
File cache = new File(dataDir, CODE_CACHE_NAME);
try {
mkdirChecked(cache);
} catch (IOException e) {
//使用/data/data/<package>/files/code-cache目录
cache = new File(context.getFilesDir(), CODE_CACHE_NAME);
mkdirChecked(cache);
}
File dexDir = new File(cache, secondaryFolderName);
mkdirChecked(dexDir);
return dexDir;
}
回到doInstallation
继续看注释4通过MultiDexExtractor
提取缓存文件,然后注释5安装提取的dex。这两个步骤是整个MultiDex.install(Context)的过程中最重要的。接下来看看MultiDexExtractor
MultiDexExtractor.load
获取可以安装的dex文件列表,可以是上次解压出来的缓存文件,也可以是重新从Apk包里面提取出来的。
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;
//是否是强制性提取或者源文件(覆盖安装)发生了变化
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 {
//重新解压,并保存解压出来的dex文件的信息
if (forceReload) {
Log.i(TAG, "Forced extraction must be performed.");
} else {
Log.i(TAG, "Detected that extraction must be performed.");
}
files = performExtractions();
putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), sourceCrc,
files);
}
Log.i(TAG, "load found " + files.size() + " secondary dex files");
return files;
}
主要是先加载之前已经提取过的dex文件loadExistingExtractions
,如果加载失败后执行真正的提取过程performExtractions
,然后保存dex文件信息putStoredApkInfo
。
MultiDexExtractor.loadExistingExtractions
private List<MultiDexExtractor.ExtractedDex> loadExistingExtractions(Context context, String prefsKeyPrefix) throws IOException {
Log.i("MultiDex", "loading existing secondary dex files");
String extractedFilePrefix = this.sourceApk.getName() + ".classes";
SharedPreferences multiDexPreferences = getMultiDexPreferences(context);
int totalDexNumber = multiDexPreferences.getInt(prefsKeyPrefix + "dex.number", 1);
List<MultiDexExtractor.ExtractedDex> files = new ArrayList(totalDexNumber - 1);
for(int secondaryNumber = 2; secondaryNumber <= totalDexNumber; ++secondaryNumber) {
String fileName = extractedFilePrefix + secondaryNumber + ".zip";
MultiDexExtractor.ExtractedDex extractedFile = new MultiDexExtractor.ExtractedDex(this.dexDir, fileName);
if (!extractedFile.isFile()) {
throw new IOException("Missing extracted secondary dex file '" + extractedFile.getPath() + "'");
}
extractedFile.crc = getZipCrc(extractedFile);
long expectedCrc = multiDexPreferences.getLong(prefsKeyPrefix + "dex.crc." + secondaryNumber, -1L);
long expectedModTime = multiDexPreferences.getLong(prefsKeyPrefix + "dex.time." + secondaryNumber, -1L);
long lastModified = extractedFile.lastModified();
if (expectedModTime != lastModified || expectedCrc != extractedFile.crc) {
throw new IOException("Invalid extracted dex: " + extractedFile + " (key \"" + prefsKeyPrefix + "\"), expected modification time: " + expectedModTime + ", modification time: " + lastModified + ", expected crc: " + expectedCrc + ", file crc: " + extractedFile.crc);
}
files.add(extractedFile);
}
return files;
}
从dexDir路径(/data/data/pkgName/code_cache/secondary-dexes)下提取如下zip文件列表:
/data/data/pkgName/code_cache/secondary-dexes/apkName.apk.classes2.zip
/data/data/pkgName/code_cache/secondary-dexes/apkName.apk.classes3.zip
……
/data/data/pkgName/code_cache/secondary-dexes/apkName.apk.classesN.zip
MultiDexExtractor.performExtractions
private List<ExtractedDex> performExtractions() throws IOException {
//匹配的前缀,示例:com.nercita.mpcms-1.apk.classes
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;
//从sourceApk的文件中找到classes2.dex…classesN.dex的ZipEntry入口,依次调用extract(apk, dexFile, extractedFile, extractedFilePrefix):
ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
while (dexFile != null) {
//com.nercita.mpcms-1.apk.classes2.zip
String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
//extractedFile路径为data/data/com.nercita.mpcms/code_cache/secondary-dexes/com.nercita.mpcms-1.apk.classes2.zip
ExtractedDex extractedFile = new ExtractedDex(dexDir, fileName);
files.add(extractedFile);
Log.i(TAG, "Extraction is needed for file " + extractedFile);
int numAttempts = 0;
boolean isExtractionSuccessful = false;
//每个dex的提取都尝试三次;
while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) {
numAttempts++;
// Create a zip file (extractedFile) containing only the secondary dex file
// (dexFile) from the apk.
//真正的提取。将源Apk解压,将非主Dex文件写为zip文件。
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;
}
其中loadExistingExtractions
和putStoredApkInfo
方法是通过SharedPreferences
读写dex文件的数目totalDexNumber
,apk文件的crc值,修改时间。putStoredApkInfo
后,下一次直接loadExistingExtractions
就可以了
MultiDexExtractor.extract
private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo,
String extractedFilePrefix) throws IOException, FileNotFoundException {
InputStream in = apk.getInputStream(dexFile);
ZipOutputStream out = null;
// Temp files must not start with extractedFilePrefix to get cleaned up in prepareDexDir()
File tmp = File.createTempFile("tmp-" + extractedFilePrefix, EXTRACTED_SUFFIX,
extractTo.getParentFile());
Log.i(TAG, "Extracting " + tmp.getPath());
try {
out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp)));
try {
ZipEntry classesDex = new ZipEntry("classes.dex");
// keep zip entry time since it is the criteria used by Dalvik
classesDex.setTime(dexFile.getTime());
out.putNextEntry(classesDex);
byte[] buffer = new byte[BUFFER_SIZE];
int length = in.read(buffer);
while (length != -1) {
out.write(buffer, 0, length);
length = in.read(buffer);
}
out.closeEntry();
} finally {
out.close();
}
if (!tmp.setReadOnly()) {
throw new IOException("Failed to mark readonly \"" + tmp.getAbsolutePath() +
"\" (tmp of \"" + extractTo.getAbsolutePath() + "\")");
}
Log.i(TAG, "Renaming to " + extractTo.getPath());
if (!tmp.renameTo(extractTo)) {
throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() +
"\" to \"" + extractTo.getAbsolutePath() + "\"");
}
} finally {
closeQuietly(in);
tmp.delete(); // return status ignored
}
}
参数如下:
- apk: Apk文件/data/app/apkName.apk
- dexFile: Apk文件zip解压后得到的从dex文件,classes2.dex…classesN.dex
- extractedFile: dexFile写入的目标文件/data/data/pkgName/code_cache/secondary-dexes/apkName.apk.classes2.zip等
- extractedFilePrefix: 前缀apkName.apk.classes
将Apk文件解压后得到的classes2.dex, …, classesN.dex文件的内容依次写入到/data/data/pkgName/code_cache/secondary-dexes/apkName.apk.classes2.zip, …, /data/data/pkgName/code_cache/secondary-dexes/apkName.apk.classesN.zip压缩文件的classes.dex文件中。
MultiDex.installSecondaryDexes
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);
}
}
}
在不同的SDK版本上,ClassLoader(更准确来说是DexClassLoader)加载dex文件的方式有所不同,所以这里做了V4/V14/V19的兼容(Magic Code)
V19.install
private static final class V19 {
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>();
//扩展DexPathList中的dexElements数组字段;
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;
}
}
/**
* A wrapper around
* {@code private static final dalvik.system.DexPathList#makeDexElements}.
*/
//将刚刚提取出来的zip文件包装成Element对象
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);
}
//合并到一个新的数组
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);
}
}
- 反射获取ClassLoader中的pathList字段;
- 反射调用DexPathList对象中的makeDexElements方法,将刚刚提取出来的zip文件包装成Element对象;
- 将包装成的Element对象扩展到DexPathList中的dexElements数组字段里;
- makeDexElements中有dexopt的操作,是一个耗时的过程,产物是一个优化过的odex文件
参考资料: