0x01 开篇
官方文档MultiDex解释:
1.Dalvik Executable (DEX)文件的总方法数限制在65536以内,其中包括Android framwork method, lib method,还有你的 code method ,所以请使用MultiDex。
2.对于5.0以下版本,请使用multidex support library。
3.而5.0及以上版本,由于ART模式的存在,app第一次安装之后会进行一次预编译(pre-compilation) ,如果这时候发现了classes(..N).dex文件的存在就会将他们最终合成为一个.oat的文件。
官方文档还写明了multiDex support lib 的使用局限:
1.在应用安装到手机上的时候dex文件的安装是复杂的(complex)有可能会因为第二个dex文件太大导致ANR。请用proguard优化你的代码。
2.使用了mulitDex的App有可能在4.0(api level 14)以前的机器上无法启动,因为Dalvik linearAlloc bug。用proguard优化你的代码将减少该bug几率。
3.使用了mulitDex的App在runtime期间有可能因为Dalvik linearAlloc limit Crash。该内存分配限制在 4.0版本被增大,但是5.0以下的机器上的Apps依然会存在这个限制。
4.主dex被dalvik虚拟机执行时候,哪些类必须在主dex文件里面这个问题比较复杂。build tools 可以搞定这个问题。但是如果你代码存在反射和native的调用也不保证100%正确。
虽然看起来好像有点坑的样子,但是不管了,先嫖再给钱,看看整个流程先~
0x02 好戏开场了
在android5.0之前,整个app里就一个dex,对这个dex的加载在安装的时候就加载好了,但是这个dex的方法数量被限制在65535之内。
引入MultiDex之后,会分为一个主dex和多个分dex(classes1.dex,classes2.dex,classesN.dex),应用首次安装会加载主dex,而第二个以及之后的dex则是在Application里面的attachBaseContext()里面调用MultiDex.install(this)的时候进行解压与安装【安装的概念就是对原始的apk文件中包含的dex包进行解压优化并进行优化目录归档,让应用下次启动可以在特定目录下找到这批dex包进行ClassLoader的初始化】
这个方法里面主要做了两件事,接下来我们通过源码来进行分析:
public static void install(Context context) {
Log.i("MultiDex", "Installing application");
if (IS_VM_MULTIDEX_CAPABLE) {
Log.i("MultiDex", "VM has multidex support, MultiDex support library is disabled.");
} else if (VERSION.SDK_INT < 4) {
throw new RuntimeException("MultiDex installation failed. SDK " + VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + ".");
} else {
try {
ApplicationInfo applicationInfo = getApplicationInfo(context);
if (applicationInfo == null) {
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", "");
} catch (Exception var2) {
Log.e("MultiDex", "MultiDex installation failure", var2);
throw new RuntimeException("MultiDex installation failed (" + var2.getMessage() + ").");
}
Log.i("MultiDex", "install done");
}
}
首先通过Application获取到两个文件路径,分别是applicationInfo.sourceDir
【/data/app/com.hotfix.sample-JEOYk3NmgdLD31DnXH25EQ==/base.apk】
applicationInfo.dataDir
【/data/user/0/com.hotfix.sample】
然后调用方法doInstallation(),我们接着看doInstallation()方法。
private static void doInstallation(Context mainContext, File sourceApk, File dataDir, String secondaryFolderName, String prefsKeyPrefix) throws IOException, IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
Set var5 = installedApk;
synchronized(installedApk) {
if (!installedApk.contains(sourceApk)) {
installedApk.add(sourceApk);
if (VERSION.SDK_INT > 20) {
Log.w("MultiDex", "MultiDex is not guaranteed to work in SDK version " + VERSION.SDK_INT + ": SDK version higher than " + 20 + " 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实例,后面要做的工作,就是把其他dex文件加载后,把其DexFile对象添加到这个ClassLoader实例里
ClassLoader loader;
try {
loader = mainContext.getClassLoader();
} catch (RuntimeException var11) {
Log.w("MultiDex", "Failure while trying to obtain Context class loader. Must be running in test mode. Skip patching.", var11);
return;
}
if (loader == null) {
Log.e("MultiDex", "Context class loader is null. Must be running in test mode. Skip patching.");
} else {
try {
//清除/data/user/0/pkgName/files/code_cache/secondary-dexes的内容
clearOldDexDir(mainContext);
} catch (Throwable var10) {
Log.w("MultiDex", "Something went wrong when trying to clear old MultiDex extraction, continuing without cleaning.", var10);
}
//先使用/data/user/0/pkgName/code_cache/secondary-dexes目录
//如果失败使用/data/user/0/pkgName/files/code_cache/secondary-dexes
//这也是为什么上面需要进行clearOldDexDir的原因
File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);
//加载Dex文件列表
List extends File> files = MultiDexExtractor.load(mainContext, sourceApk, dexDir, prefsKeyPrefix, false);
//开始安装
installSecondaryDexes(loader, dexDir, files);
}
}
}
}
MultiDex.install(Context)的过程中,关键的步骤就是MultiDexExtractor#load方法和MultiDex#installSecondaryDexes方法,继续看看MultiDexExtractor#load方法做了些什么见不得人的事:
static List extends File> load(Context context, File sourceApk, File dexDir, String prefsKeyPrefix, boolean forceReload) throws IOException {
Log.i("MultiDex", "MultiDexExtractor.load(" + sourceApk.getPath() + ", " + forceReload + ", " + prefsKeyPrefix + ")");
long currentCrc = getZipCrc(sourceApk);//获取当前Apk文件的crc值
File lockFile = new File(dexDir, "MultiDex.lock");
RandomAccessFile lockRaf = new RandomAccessFile(lockFile, "rw");
FileChannel lockChannel = null;
FileLock cacheLock = null;
IOException releaseLockException = null;
List files;
try {
lockChannel = lockRaf.getChannel();
Log.i("MultiDex", "Blocking on lock " + lockFile.getPath());
cacheLock = lockChannel.lock();//加上文件锁,防止多进程冲突
Log.i("MultiDex", lockFile.getPath() + " locked");
// 先判断是否强制重新解压,这里第一次会优先使用已解压过的dex文件,如果加载失败就强制重新解压
// 此外,通过crc和文件修改时间,判断如果Apk文件已经被修改(覆盖安装),就会跳过缓存重新解压dex文件
if (!forceReload && !isModified(context, sourceApk, currentCrc, prefsKeyPrefix)) {
try {
// 加载缓存的dex文件
files = loadExistingExtractions(context, sourceApk, dexDir, prefsKeyPrefix);
} catch (IOException var21) {
Log.w("MultiDex", "Failed to reload existing extracted secondary dex files, falling back to fresh extraction", var21);
//加载失败的话重新解压
files = performExtractions(sourceApk, dexDir);
//保存解压出来的dex文件的信息[SP保存]
putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), currentCrc, files);
}
} else {
Log.i("MultiDex", "Detected that extraction must be performed.");
files = performExtractions(sourceApk, dexDir);
putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), currentCrc, files);
}
} finally {
if (cacheLock != null) {
try {
cacheLock.release();
} catch (IOException var20) {
Log.e("MultiDex", "Failed to release lock on " + lockFile.getPath());
releaseLockException = var20;
}
}
if (lockChannel != null) {
closeQuietly(lockChannel);
}
closeQuietly(lockRaf);
}
if (releaseLockException != null) {
throw releaseLockException;
} else {
Log.i("MultiDex", "load found " + files.size() + " secondary dex files");
return files;
}
}
这个过程主要是获取可以安装的dex文件列表,可以是上次解压出来的缓存文件,也可以是重新从Apk包里面提取出来的。在performExtractions()方法中对Dex进行解压,这里会有明显的耗时,而且解压出来的dex文件,会被压缩成.zip压缩包,压缩的过程也会有明显的耗时(这里压缩dex文件可能是问了节省空间)。
如果dex文件是重新解压出来的,则会保存dex文件的信息,包括解压的apk文件的crc值、修改时间以及dex文件的数目,以便下一次启动直接使用已经解压过的dex缓存文件,而不是每一次都重新解压。
private static List performExtractions(File sourceApk, File dexDir) throws IOException {
String extractedFilePrefix = sourceApk.getName() + ".classes";
prepareDexDir(dexDir, extractedFilePrefix);
List files = new ArrayList();
ZipFile apk = new ZipFile(sourceApk);
try {
int secondaryNumber = 2;
for(ZipEntry dexFile = apk.getEntry("classes" + secondaryNumber + ".dex"); dexFile != null; dexFile = apk.getEntry("classes" + secondaryNumber + ".dex")) {
String fileName = extractedFilePrefix + secondaryNumber + ".zip";
MultiDexExtractor.ExtractedDex extractedFile = new MultiDexExtractor.ExtractedDex(dexDir, fileName);
files.add(extractedFile);
Log.i("MultiDex", "Extraction is needed for file " + extractedFile);
int numAttempts = 0;
boolean isExtractionSuccessful = false;
while(numAttempts < 3 && !isExtractionSuccessful) {
++numAttempts;
extract(apk, dexFile, extractedFile, extractedFilePrefix);
try {
extractedFile.crc = getZipCrc(extractedFile);
isExtractionSuccessful = true;
} catch (IOException var19) {
isExtractionSuccessful = false;
Log.w("MultiDex", "Failed to read crc from " + extractedFile.getAbsolutePath(), var19);
}
Log.i("MultiDex", "Extraction " + (isExtractionSuccessful ? "succeeded" : "failed") + " - length " + extractedFile.getAbsolutePath() + ": " + extractedFile.length() + " - crc: " + extractedFile.crc);
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 {
try {
apk.close();
} catch (IOException var18) {
Log.w("MultiDex", "Failed to close resource", var18);
}
}
return files;
}
那现在回到之前的MultiDex#install方法,通过一系列的处理,我们拿到了一批经过处理的dex【.zip】,接下来的事情就是把这批dex搞进去ClassLoader里面了,installSecondaryDexes(loader, dexDir, files);
private static void installSecondaryDexes(ClassLoader loader, File dexDir, List extends File> files) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
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);
}
}
}
这个方法做了android的版本判断,我们选择>=19的分支来看看
private static void install(ClassLoader loader, List extends File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
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));
if (suppressedExceptions.size() > 0) {
Iterator var6 = suppressedExceptions.iterator();
while(var6.hasNext()) {
IOException e = (IOException)var6.next();
Log.w("MultiDex", "Exception in makeDexElement", e);
}
Field suppressedExceptionsField = MultiDex.findField(dexPathList, "dexElementsSuppressedExceptions");
IOException[] dexElementsSuppressedExceptions = (IOException[])((IOException[])suppressedExceptionsField.get(dexPathList));
if (dexElementsSuppressedExceptions == null) {
dexElementsSuppressedExceptions = (IOException[])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);
}
}
调用DexPathList#makeDexElements方法,可以加载我们上面解压得到的dex文件,从代码也可以看出,DexPathList#makeDexElements其实也是通过调用DexFile#loadDex来加载dex文件并创建DexFile对象的。
V19中,通过反射调用DexPathList#makeDexElements方法加载我们需要的dex文件,在把加载得到的数组扩展到ClassLoader实例的"pathList"字段,从而完成dex文件的安装。如果在执行DexPathList#makeDexElements方法的过程中出现异常,后面使用反射的方式把这些异常记录进DexPathList的dexElementsSuppressedExceptions字段里面。
在创建DexFile对象的时候,都需要通过DexFile的Native方法openDexFile来打开dex文件。这个过程的主要目的是给当前的dex文件做Optimize优化处理并生成相同文件名的odex文件,App实际加载类的时候,都是通过odex文件进行的。
0x03 总结一下
MulitDex.install主要完成这样一个流程~
1.从data/app/pkgName-x/base.apk中加载其他分dex,存储目录首选为/data/user/0/pkgName/code_cache/secondary-dexes或者是备用目录/data/user/0/pkgName/files/code_cache/secondary-dexes。
在首次安装完成了除了主dex之外的其他n个dex包的解压并生成一个dex的文件列表对象dex-file-list。
2.MulitDex#installSecondaryDexes安装dex。上一步我们已经加载获取到dex-file-list,接下来做的就是makeDexElements【使用DexFile对dex进行加载并进行进一步的封装,为添加到ClassLoader的DexElments数组中做准备】,makeDexElements做的事就是将上一步处理完成的dex包装成结构体DexFile并插入到ClassLoader的DexElments中。
在创建DexFile对象的时候,都需要通过DexFile的Native方法openDexFile来打开dex文件,这个过程的主要目的是给当前的dex文件做Optimize优化处理并生成相同文件名的odex文件。主dex的优化我们已经在安装apk的时候完成了,其余的dex就是在MultiDex#installSecondaryDexes里面优化的,而后者也是MultiDex过程中,另外一个耗时比较多的操作。
0x04 我所遇到的问题
- 在学习dex包相关的时候,我对于apk中dex包的处理流程并不清楚,我所困惑的是,dex包从apk安装经历了什么处理?这些dex包最后去了哪里?现在回答一下上面的问题:
我们把apk当成一个zip压缩包,dalvik每次加载apk都要从中解压出class.dex文件,加载过程还涉及到dex的classes需要的杂七杂八的依赖库的加载,这是非常耗时的操作。于是Android决定优化一下这个问题,在app安装到手机之后,系统运行dexopt程序对dex进行优化,将dex的依赖库文件和一些辅助数据打包成odex文件。存放在cache/dalvik_cache目录下。保存格式为apk路径 @ apk名 @ classes.dex。这样以空间换时间大大缩短读取/加载dex文件的过程。那multidex方案在对除了主dex之外的其他dex进行解压优化之后应该也会存放到这个目录中,在MultiDex流程中有个步奏是获取odex的缓存文件,应该也是从这个目录下面加载一批dex的文件列表并装载进入ClassLoader中。
- 在学习Tinker相关原理的时候涉及到dex包的合成,那个时候有个问题不能理解的是,tinker进行了dex的合成,然后存在于哪里,加载时机又是怎么样的?
tinker的差分包会在apk原始路径下面找到差分的dex【比如我热修的类处于classes2.dex】,那会在/data/app/pkgname==/base.apk里面找到原始的dex包然后进行合并,我们把这个新生成的dex称为new_dex,这个new_dex是放在tinker的自定义目录下的,在启动load的过程中,我们在classloader加载完成基准包的dex列表之后生成classloader,再通过类似于MultiDex的方式对这个new_dex进行处理并放在DexElements的最前从而达到热修复的目的。
0x05 推荐阅读
你不知道的MultiDex有多坑:https://www.jianshu.com/p/a5353748159f