1、基本介绍:
- 当Android系统启动一个应用的时候,有一步是对Dex进行优化,这个过程有一个专门的工具来处理,叫DexOpt。DexOpt的执行过程是在第一次加载Dex文件的时候执行的。这个过程会生成一个ODEX文件,即Optimised Dex。执行ODex的效率会比直接执行Dex文件的效率要高很多。
- 但是在早期的Android系统中,DexOpt有一个问题,也就是这篇文章想要说明并解决的问题。DexOpt会把每一个类的方法id检索起来,存在一个链表结构里面。但是这个链表的长度是用一个short类型来保存的,导致了方法id的数目不能够超过65536个。当一个项目足够大的时候,显然这个方法数的上限是不够的。尽管在新版本的Android系统中,DexOpt修复了这个问题,但是我们仍然需要对老系统做兼容。
- 这些 method 是指能够 索引 (reference) 到的,而不是 定义 (define) 的。或者说,如果你定义了一个方法,但这个方法并没有被调用,那么就不算在内。
- 这些 method 不仅仅是开发人员自己写的,还包括所有第三方 library 里面的 method。
2、解决办法:
Multidex:
官方提供的解决方案,这篇文章 里有详细的使用方法,此不赘述。
ProGuard:
ProGuard可以把 code 里 unnecessary 的 method 移除,压缩 apk,当然还有 代码混淆 的奇效。
再创建一个 DEX File:
把 app 里可以独立的模块或 code 提取出来,放到一个独立的 dex 文件里,你可以使用Custom ClassLoader 来加载这些类,然后使用 接口 或反射 来调用这些方法。不过,这个过程还是比较麻烦的。
3、Multidex原理:
3.1、工作流程:MultiDex的工作流程具体分为两个部分
- 一个部分是打包构建Apk的时候,将Dex文件拆分成若干个小的Dex文件,这个Android Studio已经帮我们做了(设置 “multiDexEnabled true”)。
- 另一部分就是在启动Apk的时候,同时加载多个Dex文件(具体是加载Dex文件优化后的Odex文件,不过文件名还是.dex),这一部分工作从Android 5.0开始系统已经帮我们做了,但是在Android 5.0以前还是需要通过MultiDex Support库来支持(MultiDex.install(Context))。
3.2、MultiDex.install(Context):
public static void install(Context context) {
Log.i(TAG, "install");
// 1. 判读是否需要执行MultiDex。
if (IS_VM_MULTIDEX_CAPABLE) {
Log.i(TAG, "VM has multidex support, MultiDex support library is disabled.");
return;
}
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);
if (applicationInfo == null) {
// Looks like running on a test Context, so just return without patching.
return;
}
// 2. 如果这个方法已经调用过一次,就不能再调用了。
synchronized (installedApk) {
String apkPath = applicationInfo.sourceDir;
if (installedApk.contains(apkPath)) {
return;
}
installedApk.add(apkPath);
// 3. 如果当前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") + "\"");
}
// 4. 获取当前的ClassLoader实例,后面要做的工作,就是把其他dex文件加载后,
// 把其DexFile对象添加到这个ClassLoader实例里就完事了。
ClassLoader loader;
try {
loader = context.getClassLoader();
} catch (RuntimeException e) {
Log.w(TAG, "Failure while trying to obtain Context class loader. " +
"Must be running in test mode. Skip patching.", e);
return;
}
if (loader == null) {
Log.e(TAG,
"Context class loader is null. Must be running in test mode. "
+ "Skip patching.");
return;
}
try {
// 5. 清除旧的dex文件,注意这里不是清除上次加载的dex文件缓存。
// 获取dex缓存目录是,会优先获取/data/data/<package>/code-cache作为缓存目录。
// 如果获取失败,则使用/data/data/<package>/files/code-cache目录。
// 这里清除的是/data/data/<package>/files/code-cache目录。
clearOldDexDir(context);
} catch (Throwable t) {
Log.w(TAG, "Something went wrong when trying to clear old MultiDex extraction, "
+ "continuing without cleaning.", t);
}
// 6. 获取缓存目录(/data/data/<package>/code-cache)。
File dexDir = getDexDir(context, applicationInfo);
// 7. 加载缓存文件(如果有)。
List<File> files = MultiDexExtractor.load(context, applicationInfo, dexDir, false);
// 8. 检查缓存的dex是否安全
if (checkValidZipFiles(files)) {
// 9. 安装缓存的dex
installSecondaryDexes(loader, dexDir, files);
} else {
// 9. 从apk压缩包里面提取dex文件
Log.w(TAG, "Files were not valid zip files. Forcing a reload.");
files = MultiDexExtractor.load(context, applicationInfo, dexDir, true);
if (checkValidZipFiles(files)) {
// 10. 安装提取的dex
installSecondaryDexes(loader, dexDir, files);
} else {
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");
}
- 1、MultiDex#clearOldDexDir(Context)方法:
- 这个方法的作用是删除/data/data//files/code-cache,一开始我以为这个方法是删除上一次执行MultiDex后的缓存文件,不过这明显不对,不可能每次MultiDex都重新解压dex文件一边,这样每次启动会很耗时,只有第一次冷启动的时候才需要解压dex文件。后来我又想是不是以前旧版的MultiDex曾经把缓存文件放在这个目录里,现在新版本只是清除以前旧版的遗留文件?但是我找遍了整个MultiDex Repo的提交也没有见过类似的旧版本代码。后面我仔细看MultiDex#getDexDir这个方法才发现,原来MultiDex在获取dex缓存目录是,会优先获取/data/data//code-cache作为缓存目录,如果获取失败,则使用/data/data//files/code-cache目录,而后者的缓存文件会在每次App重新启动的时候被清除。
- 2、MultiDexExtractor#load()方法:
-
-
这个过程主要是获取可以安装的dex文件列表,可以是上次解压出来的缓存文件,也可以是重新从Apk包里面提取出来的。需要注意的时,如果是重新解压,这里会有明显的耗时,而且解压出来的dex文件,会被压缩成.zip压缩包,压缩的过程也会有明显的耗时(这里压缩dex文件可能是问了节省空间)。 如果dex文件是重新解压出来的,则会保存dex文件的信息,包括解压的apk文件的crc值、修改时间以及dex文件的数目,以便下一次启动直接使用已经解压过的dex缓存文件,而不是每一次都重新解压。
static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir,
boolean forceReload) throws IOException {
Log.i(TAG, "MultiDexExtractor.load(" + applicationInfo.sourceDir + ", " + forceReload + ")");
final File sourceApk = new File(applicationInfo.sourceDir);
// 1. 获取当前Apk文件的crc值。
long currentCrc = getZipCrc(sourceApk);
// Validity check and extraction must be done only while the lock file has been taken.
File lockFile = new File(dexDir, LOCK_FILENAME);
RandomAccessFile lockRaf = new RandomAccessFile(lockFile, "rw");
FileChannel lockChannel = null;
FileLock cacheLock = null;
List<File> files;
IOException releaseLockException = null;
try {
lockChannel = lockRaf.getChannel();
Log.i(TAG, "Blocking on lock " + lockFile.getPath());
// 2. 加上文件锁,防止多进程冲突。
cacheLock = lockChannel.lock();
Log.i(TAG, lockFile.getPath() + " locked");
// 3. 先判断是否强制重新解压,这里第一次会优先使用已解压过的dex文件,如果加载失败就强制重新解压。
// 此外,通过crc和文件修改时间,判断如果Apk文件已经被修改(覆盖安装),就会跳过缓存重新解压dex文件。
if (!forceReload && !isModified(context, sourceApk, currentCrc)) {
try {
// 4. 加载缓存的dex文件
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);
// 5. 加载失败的话重新解压,并保存解压出来的dex文件的信息。
files = performExtractions(sourceApk, dexDir);
putStoredApkInfo(context,
getTimeStamp(sourceApk), currentCrc, files.size() + 1);
}
} else {
// 4. 重新解压,并保存解压出来的dex文件的信息。
Log.i(TAG, "Detected that extraction must be performed.");
files = performExtractions(sourceApk, dexDir);
putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
}
} finally {
if (cacheLock != null) {
try {
cacheLock.release();
} catch (IOException e) {
Log.e(TAG, "Failed to release lock on " + lockFile.getPath());
// Exception while releasing the lock is bad, we want to report it, but not at
// the price of overriding any already pending exception.
releaseLockException = e;
}
}
if (lockChannel != null) {
closeQuietly(lockChannel);
}
closeQuietly(lockRaf);
}
if (releaseLockException != null) {
throw releaseLockException;
}
Log.i(TAG, "load found " + files.size() + " secondary dex files");
return files;
}
3、MultiDex#installSecondaryDexes()方法:
- 在不同的SDK版本上,ClassLoader(更准确来说是DexClassLoader)加载dex文件的方式有所不同。
DexClassLoader会使用一个DexPathList类来封装DexFile数组。通过调用DexPathList#makeDexElements方法,可以加载我们上面解压得到的dex文件,从代码也可以看出,DexPathList#makeDexElements其实也是通过调用DexFile#loadDex来加载dex文件并创建DexFile对象的。V14中,通过反射调用DexPathList#makeDexElements方法加载我们需要的dex文件,在把加载得到的数组扩展到ClassLoader实例的”pathList”字段,从而完成dex文件的安装。
ClassLoader是支持直接加载.dex/.zip/.jar/.apk的dex文件包的。在创建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文件。)
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);
}
}
}