MultiDex工作原理分析和优化方案

动态加载技术(插件化)系列已经坑了有一段时间了,不过UP主我并没有放弃治疗哈,相信在不就的未来就可以看到“系统Api Hook模式”和插件化框架Frontia的更新了。今天要讲的是动态加载技术的亲戚 —— MultiDex。他们的核心原理之一都是dex文件的加载。

MultiDex是Google为了解决“65535方法数超标”以及“INSTALL_FAILED_DEXOPT”问题而开发的一个Support库,具体如何使用MultiDex现在市面已经有一大堆教程(可以参考给 App 启用 MultiDex 功能),这里不再赘述。这篇日志主要是配合源码分析MultiDex的工作原理,以及提供一些MultiDex优化的方案。

Dex的工作机制

等等,这个章节讲的不是MultiDex吗,怎么变成Dex了?没错哈,没有Dex,哪来的MultiDex。在Android中,对Dex文件操作对应的类叫做DexFile。在CLASSLOADER 的工作机制中,我们说到:

对于 Java 程序来说,编写程序就是编写类,运行程序也就是运行类(编译得到的class文件),其中起到关键作用的就是类加载器 ClassLoader。

Android程序的每一个Class都是由ClassLoader#loadClass方法加载进内存的,更准确来说,一个ClassLoader实例会有一个或者多个DexFile实例,调用了ClassLoader#loadClass之后,ClassLoader会通过类名,在自己的DexFile数组里面查找有没有那个DexFile对象里面存在这个类,如果都没有就抛ClassNotFound异常。ClassLoader通过调用DexFile的一个叫defineClass的Native方法去加载指定的类,这点与JVM略有不同,后者是直接调用ClassLoader#defineCLass方法,反正最后实际加载类的方法都叫defineClass就没错了🌝。

创建DexFile对象

首先来看看造DexFile对象的构方法。

  1. public final class DexFile {
  2.     private int mCookie;
  3.     private final String mFileName;
  4.     …
  5.     public DexFile(File file) throws IOException {
  6.         this(file.getPath());
  7.     }
  8.     public DexFile(String fileName) throws IOException {
  9.         mCookie = openDexFile(fileName, null, 0);
  10.         mFileName = fileName;
  11.         guard.open(“close”);
  12.     }
  13.     private DexFile(String sourceName, String outputName, int flags) throws IOException {
  14.         mCookie = openDexFile(sourceName, outputName, flags);
  15.         mFileName = sourceName;
  16.         guard.open(“close”);
  17.     }
  18.     static public DexFile loadDex(String sourcePathName, String outputPathName,
  19.         int flags) throws IOException {
  20.         return new DexFile(sourcePathName, outputPathName, flags);
  21.     }
  22.     public Class loadClass(String name, ClassLoader loader) {
  23.         String slashName = name.replace(‘.’‘/’);
  24.         return loadClassBinaryName(slashName, loader);
  25.     }
  26.     public Class loadClassBinaryName(String name, ClassLoader loader) {
  27.         return defineClass(name, loader, mCookie);
  28.     }
  29.     private native static Class defineClass(String name, ClassLoader loader, int cookie);
  30.     native private static int openDexFile(String sourceName, String outputName,
  31.         int flags) throws IOException;
  32.     native private static int openDexFile(byte[] fileContents)
  33.     …
  34. }

通过以前分析过的源码,我们知道ClassLoader主要是通过DexFile.loadDex这个静态方法来创建它需要的DexFile实例的,这里创建DexFile的时候,保存了Dex文件的文件路径mFileName,同时调用了openDexFile的Native方法打开Dex文件并返回了一个mCookie的整型变量(我不知道这个干啥用的,我猜它是一个C++用的资源句柄,用于Native层访问具体的Dex文件)。在Native层的openDexFile方法里,主要做了检查当前创建来的Dex文件是否是有效的Dex文件,还是是一个带有Dex文件的压缩包,还是一个无效的Dex文件。

加载Dex文件里的类

加载类的时候,ClassLoader又是通过DexFile#loadClass这个方法来完成的,这个方法里调用了defineClass这个Native方法,看来DexFile才是加载Class的具体API,加载Dex文件和加载具体Class都是通过Native方法完成,ClassLoader有点名不副实啊。

MultiDex的工作机制

当一个Dex文件太肥的时候(方法数目太多、文件太大),在打包Apk文件的时候就会出问题,就算打包的时候不出问题,在Android 5.0以下设备上安装或运行Apk也会出问题(具体原因可以参考给 App 启用 MultiDex 功能)。既然一个Dex文件不行的话,那就把这个硕大的Dex文件拆分成若干个小的Dex文件,刚好一个ClassLoader可以有多个DexFile,这就是MultiDex的基本设计思路。

工作流程

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 Support版本是com.android.support:multidex:1.0.1,但是现在仓库的master分支已经有了许多新的提交(其中最明显的区别是加入了FileLock来控制多进程同步问题),所以这里分析的源码都是最新的master分支上的。

MultiDex Support的入口是MultiDex.install(Context),先从这里入手吧。(这次我把具体的分析都写在代码的注释了,这样看是不是更简洁明了些?)

  1. public static void install(Context context) {
  2.         Log.i(TAG, “install”);
  3.         // 1. 判读是否需要执行MultiDex。
  4.         if (IS_VM_MULTIDEX_CAPABLE) {
  5.             Log.i(TAG, “VM has multidex support, MultiDex support library is disabled.”);
  6.             return;
  7.         }
  8.         if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) {
  9.             throw new RuntimeException(“Multi dex installation failed. SDK “ + Build.VERSION.SDK_INT
  10.                     + ” is unsupported. Min SDK version is “ + MIN_SDK_VERSION + “.”);
  11.         }
  12.         try {
  13.             ApplicationInfo applicationInfo = getApplicationInfo(context);
  14.             if (applicationInfo == null) {
  15.                 // Looks like running on a test Context, so just return without patching.
  16.                 return;
  17.             }
  18.             // 2. 如果这个方法已经调用过一次,就不能再调用了。
  19.             synchronized (installedApk) {
  20.                 String apkPath = applicationInfo.sourceDir;
  21.                 if (installedApk.contains(apkPath)) {
  22.                     return;
  23.                 }
  24.                 installedApk.add(apkPath);
  25.                 // 3. 如果当前Android版本已经自身支持了MultiDex,依然可以执行MultiDex操作,
  26.                 // 但是会有警告。
  27.                 if (Build.VERSION.SDK_INT > MAX_SUPPORTED_SDK_VERSION) {
  28.                     Log.w(TAG, “MultiDex is not guaranteed to work in SDK version “
  29.                             + Build.VERSION.SDK_INT + “: SDK version higher than “
  30.                             + MAX_SUPPORTED_SDK_VERSION + ” should be backed by “
  31.                             + “runtime with built-in multidex capabilty but it’s not the “
  32.                             + “case here: java.vm.version=\””
  33.                             + System.getProperty(“java.vm.version”) + “\””);
  34.                 }
  35.                 // 4. 获取当前的ClassLoader实例,后面要做的工作,就是把其他dex文件加载后,
  36.                 // 把其DexFile对象添加到这个ClassLoader实例里就完事了。
  37.                 ClassLoader loader;
  38.                 try {
  39.                     loader = context.getClassLoader();
  40.                 } catch (RuntimeException e) {
  41.                     Log.w(TAG, “Failure while trying to obtain Context class loader. “ +
  42.                             “Must be running in test mode. Skip patching.”, e);
  43.                     return;
  44.                 }
  45.                 if (loader == null) {
  46.                     Log.e(TAG,
  47.                             “Context class loader is null. Must be running in test mode. “
  48.                             + “Skip patching.”);
  49.                     return;
  50.                 }
  51.                 try {
  52.                   // 5. 清除旧的dex文件,注意这里不是清除上次加载的dex文件缓存。
  53.                   // 获取dex缓存目录是,会优先获取/data/data/<package>/code-cache作为缓存目录。
  54.                   // 如果获取失败,则使用/data/data/<package>/files/code-cache目录。
  55.                   // 这里清除的是第二个目录。
  56.                   clearOldDexDir(context);
  57.                 } catch (Throwable t) {
  58.                   Log.w(TAG, “Something went wrong when trying to clear old MultiDex extraction, “
  59.                       + “continuing without cleaning.”, t);
  60.                 }
  61.                 // 6. 获取缓存目录(/data/data/<package>/code-cache)。
  62.                 File dexDir = getDexDir(context, applicationInfo);
  63.                 // 7. 加载缓存文件(如果有)。
  64.                 List<File> files = MultiDexExtractor.load(context, applicationInfo, dexDir, false);
  65.                 // 8. 检查缓存的dex是否安全
  66.                 if (checkValidZipFiles(files)) {
  67.                     // 9. 安装缓存的dex
  68.                     installSecondaryDexes(loader, dexDir, files);
  69.                 } else {
  70.                     // 9. 从apk压缩包里面提取dex文件
  71.                     Log.w(TAG, “Files were not valid zip files.  Forcing a reload.”);
  72.                     files = MultiDexExtractor.load(context, applicationInfo, dexDir, true);
  73.                     if (checkValidZipFiles(files)) {
  74.                         // 10. 安装提取的dex
  75.                         installSecondaryDexes(loader, dexDir, files);
  76.                     } else {
  77.                         throw new RuntimeException(“Zip files were not valid.”);
  78.                     }
  79.                 }
  80.             }
  81.         } catch (Exception e) {
  82.             Log.e(TAG, “Multidex installation failure”, e);
  83.             throw new RuntimeException(“Multi dex installation failed (“ + e.getMessage() + “).”);
  84.         }
  85.         Log.i(TAG, “install done”);
  86.     }

具体代码的分析已经在上面代码的注释里给出了,从这里我们也可以看出,整个MultiDex.install(Context)的过程中,关键的步骤就是MultiDexExtractor#load方法和MultiDex#installSecondaryDexes方法。

(这部分是题外话)其中有个MultiDex#clearOldDexDir(Context)方法,这个方法的作用是删除/data/data/<package>/files/code-cache,一开始我以为这个方法是删除上一次执行MultiDex后的缓存文件,不过这明显不对,不可能每次MultiDex都重新解压dex文件一边,这样每次启动会很耗时,只有第一次冷启动的时候才需要解压dex文件。后来我又想是不是以前旧版的MultiDex曾经把缓存文件放在这个目录里,现在新版本只是清除以前旧版的遗留文件?但是我找遍了整个MultiDex Repo的提交也没有见过类似的旧版本代码。后面我仔细看MultiDex#getDexDir这个方法才发现,原来MultiDex在获取dex缓存目录是,会优先获取/data/data/<package>/code-cache作为缓存目录,如果获取失败,则使用/data/data/<package>/files/code-cache目录,而后者的缓存文件会在每次App重新启动的时候被清除。感觉MultiDex获取缓存目录的逻辑不是很严谨,而获取缓存目录失败也是MultiDex工作工程中少数有重试机制的地方,看来MultiDex真的是一个临时的兼容方案,Google也许并不打算认真处理这些历史的黑锅。

接下来再看看MultiDexExtractor#load这个方法。

  1. static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir,
  2.             boolean forceReload) throws IOException {
  3.         Log.i(TAG, “MultiDexExtractor.load(“ + applicationInfo.sourceDir + “, “ + forceReload + “)”);
  4.         final File sourceApk = new File(applicationInfo.sourceDir);
  5.         // 1. 获取当前Apk文件的crc值。
  6.         long currentCrc = getZipCrc(sourceApk);
  7.         // Validity check and extraction must be done only while the lock file has been taken.
  8.         File lockFile = new File(dexDir, LOCK_FILENAME);
  9.         RandomAccessFile lockRaf = new RandomAccessFile(lockFile, “rw”);
  10.         FileChannel lockChannel = null;
  11.         FileLock cacheLock = null;
  12.         List<File> files;
  13.         IOException releaseLockException = null;
  14.         try {
  15.             lockChannel = lockRaf.getChannel();
  16.             Log.i(TAG, “Blocking on lock “ + lockFile.getPath());
  17.             // 2. 加上文件锁,防止多进程冲突。
  18.             cacheLock = lockChannel.lock();
  19.             Log.i(TAG, lockFile.getPath() + ” locked”);
  20.             // 3. 先判断是否强制重新解压,这里第一次会优先使用已解压过的dex文件,如果加载失败就强制重新解压。
  21.             // 此外,通过crc和文件修改时间,判断如果Apk文件已经被修改(覆盖安装),就会跳过缓存重新解压dex文件。
  22.             if (!forceReload && !isModified(context, sourceApk, currentCrc)) {
  23.                 try {
  24.                     // 4. 加载缓存的dex文件
  25.                     files = loadExistingExtractions(context, sourceApk, dexDir);
  26.                 } catch (IOException ioe) {
  27.                     Log.w(TAG, “Failed to reload existing extracted secondary dex files,”
  28.                             + ” falling back to fresh extraction”, ioe);
  29.                     // 5. 加载失败的话重新解压,并保存解压出来的dex文件的信息。
  30.                     files = performExtractions(sourceApk, dexDir);
  31.                     putStoredApkInfo(context,
  32.                             getTimeStamp(sourceApk), currentCrc, files.size() + 1);
  33.                 }
  34.             } else {
  35.                 // 4. 重新解压,并保存解压出来的dex文件的信息。
  36.                 Log.i(TAG, “Detected that extraction must be performed.”);
  37.                 files = performExtractions(sourceApk, dexDir);
  38.                 putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
  39.             }
  40.         } finally {
  41.             if (cacheLock != null) {
  42.                 try {
  43.                     cacheLock.release();
  44.                 } catch (IOException e) {
  45.                     Log.e(TAG, “Failed to release lock on “ + lockFile.getPath());
  46.                     // Exception while releasing the lock is bad, we want to report it, but not at
  47.                     // the price of overriding any already pending exception.
  48.                     releaseLockException = e;
  49.                 }
  50.             }
  51.             if (lockChannel != null) {
  52.                 closeQuietly(lockChannel);
  53.             }
  54.             closeQuietly(lockRaf);
  55.         }
  56.         if (releaseLockException != null) {
  57.             throw releaseLockException;
  58.         }
  59.         Log.i(TAG, “load found “ + files.size() + ” secondary dex files”);
  60.         return files;
  61.     }

这个过程主要是获取可以安装的dex文件列表,可以是上次解压出来的缓存文件,也可以是重新从Apk包里面提取出来的。需要注意的时,如果是重新解压,这里会有明显的耗时,而且解压出来的dex文件,会被压缩成.zip压缩包,压缩的过程也会有明显的耗时(这里压缩dex文件可能是问了节省空间)。

如果dex文件是重新解压出来的,则会保存dex文件的信息,包括解压的apk文件的crc值、修改时间以及dex文件的数目,以便下一次启动直接使用已经解压过的dex缓存文件,而不是每一次都重新解压。

需要特别提到的是,里面的FileLock是最新的master分支里面新加进去的功能,现在最新的1.0.1版本里面是没有的。

无论是通过使用缓存的dex文件,还是重新从apk中解压dex文件,获取dex文件列表后,下一步就是安装(或者说加载)这些dex文件了。最后的工作在MultiDex#installSecondaryDexes这个方法里面。

  1. private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<File> files)
  2.             throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
  3.             InvocationTargetException, NoSuchMethodException, IOException {
  4.         if (!files.isEmpty()) {
  5.             if (Build.VERSION.SDK_INT >= 19) {
  6.                 V19.install(loader, files, dexDir);
  7.             } else if (Build.VERSION.SDK_INT >= 14) {
  8.                 V14.install(loader, files, dexDir);
  9.             } else {
  10.                 V4.install(loader, files);
  11.             }
  12.         }
  13.     }

因为在不同的SDK版本上,ClassLoader(更准确来说是DexClassLoader)加载dex文件的方式有所不同,所以这里做了V4/V14/V19的兼容(Magic Code)。

Build.VERSION.SDK_INT < 14

  1. /**
  2.      * Installer for platform versions 4 to 13.
  3.      */
  4.     private static final class V4 {
  5.         private static void install(ClassLoader loader, List<File> additionalClassPathEntries)
  6.                         throws IllegalArgumentException, IllegalAccessException,
  7.                         NoSuchFieldException, IOException {
  8.             int extraSize = additionalClassPathEntries.size();
  9.             Field pathField = findField(loader, “path”);
  10.             StringBuilder path = new StringBuilder((String) pathField.get(loader));
  11.             String[] extraPaths = new String[extraSize];
  12.             File[] extraFiles = new File[extraSize];
  13.             ZipFile[] extraZips = new ZipFile[extraSize];
  14.             DexFile[] extraDexs = new DexFile[extraSize];
  15.             for (ListIterator<File> iterator = additionalClassPathEntries.listIterator();
  16.                     iterator.hasNext();) {
  17.                 File additionalEntry = iterator.next();
  18.                 String entryPath = additionalEntry.getAbsolutePath();
  19.                 path.append(‘:’).append(entryPath);
  20.                 int index = iterator.previousIndex();
  21.                 extraPaths[index] = entryPath;
  22.                 extraFiles[index] = additionalEntry;
  23.                 extraZips[index] = new ZipFile(additionalEntry);
  24.                 extraDexs[index] = DexFile.loadDex(entryPath, entryPath + “.dex”, 0);
  25.             }
  26.             // 这个版本是最简单的。
  27.             // 只需要创建DexFile对象后,使用反射的方法分别扩展ClassLoader实例的以下字段即可。
  28.             pathField.set(loader, path.toString());
  29.             expandFieldArray(loader, “mPaths”, extraPaths);
  30.             expandFieldArray(loader, “mFiles”, extraFiles);
  31.             expandFieldArray(loader, “mZips”, extraZips);
  32.             expandFieldArray(loader, “mDexs”, extraDexs);
  33.         }
  34.     }

14 <= Build.VERSION.SDK_INT < 19

  1. /**
  2.      * Installer for platform versions 14, 15, 16, 17 and 18.
  3.      */
  4.     private static final class V14 {
  5.         private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
  6.                 File optimizedDirectory)
  7.                         throws IllegalArgumentException, IllegalAccessException,
  8.                         NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
  9.             // 扩展ClassLoader实例的“pathList”字段。
  10.             Field pathListField = findField(loader, “pathList”);
  11.             Object dexPathList = pathListField.get(loader);
  12.             expandFieldArray(dexPathList, “dexElements”, makeDexElements(dexPathList,
  13.                     new ArrayList<File>(additionalClassPathEntries), optimizedDirectory));
  14.         }
  15.         private static Object[] makeDexElements(
  16.                 Object dexPathList, ArrayList<File> files, File optimizedDirectory)
  17.                         throws IllegalAccessException, InvocationTargetException,
  18.                         NoSuchMethodException {
  19.             Method makeDexElements =
  20.                     findMethod(dexPathList, “makeDexElements”, ArrayList.class, File.class);
  21.             return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory);
  22.         }
  23.     }

从API14开始,DexClassLoader会使用一个DexpDexPathList类来封装DexFile数组。

  1. final class DexPathList {
  2.     private static final String DEX_SUFFIX = “.dex”;
  3.     private static final String JAR_SUFFIX = “.jar”;
  4.     private static final String ZIP_SUFFIX = “.zip”;
  5.     private static final String APK_SUFFIX = “.apk”;
  6.     private static Element[] makeDexElements(ArrayList<File> files,
  7.             File optimizedDirectory) {
  8.         ArrayList<Element> elements = new ArrayList<Element>();
  9.         for (File file : files) {
  10.             ZipFile zip = null;
  11.             DexFile dex = null;
  12.             String name = file.getName();
  13.             if (name.endsWith(DEX_SUFFIX)) {
  14.                 // Raw dex file (not inside a zip/jar).
  15.                 try {
  16.                     dex = loadDexFile(file, optimizedDirectory);
  17.                 } catch (IOException ex) {
  18.                     System.logE(“Unable to load dex file: “ + file, ex);
  19.                 }
  20.             } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
  21.                     || name.endsWith(ZIP_SUFFIX)) {
  22.                 try {
  23.                     zip = new ZipFile(file);
  24.                 } catch (IOException ex) {
  25.                     System.logE(“Unable to open zip file: “ + file, ex);
  26.                 }
  27.                 try {
  28.                     dex = loadDexFile(file, optimizedDirectory);
  29.                 } catch (IOException ignored) {
  30.                 }
  31.             } else {
  32.                 System.logW(“Unknown file type for: “ + file);
  33.             }
  34.             if ((zip != null) || (dex != null)) {
  35.                 elements.add(new Element(file, zip, dex));
  36.             }
  37.         }
  38.         return elements.toArray(new Element[elements.size()]);
  39.     }
  40.     private static DexFile loadDexFile(File file, File optimizedDirectory)
  41.             throws IOException {
  42.         if (optimizedDirectory == null) {
  43.             return new DexFile(file);
  44.         } else {
  45.             String optimizedPath = optimizedPathFor(file, optimizedDirectory);
  46.             return DexFile.loadDex(file.getPath(), optimizedPath, 0);
  47.         }
  48.     }
  49. }

通过调用DexPathList#makeDexElements方法,可以加载我们上面解压得到的dex文件,从代码也可以看出,DexPathList#makeDexElements其实也是通过调用DexFile#loadDex来加载dex文件并创建DexFile对象的。V14中,通过反射调用DexPathList#makeDexElements方法加载我们需要的dex文件,在把加载得到的数组扩展到ClassLoader实例的”pathList”字段,从而完成dex文件的安装。

从DexPathList的代码中我们也可以看出,ClassLoader是支持直接加载.dex/.zip/.jar/.apk的dex文件包的(我记得以前在哪篇日志中好像提到过类似的问题…)。

19 <= Build.VERSION.SDK_INT

  1. /**
  2.     * Installer for platform versions 19.
  3.     */
  4.    private static final class V19 {
  5.        private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
  6.                File optimizedDirectory)
  7.                        throws IllegalArgumentException, IllegalAccessException,
  8.                        NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
  9.            Field pathListField = findField(loader, “pathList”);
  10.            Object dexPathList = pathListField.get(loader);
  11.            ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
  12.            expandFieldArray(dexPathList, “dexElements”, makeDexElements(dexPathList,
  13.                    new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
  14.                    suppressedExceptions));
  15.            if (suppressedExceptions.size() > 0) {
  16.                for (IOException e : suppressedExceptions) {
  17.                    Log.w(TAG, “Exception in makeDexElement”, e);
  18.                }
  19.                Field suppressedExceptionsField =
  20.                        findField(dexPathList, “dexElementsSuppressedExceptions”);
  21.                IOException[] dexElementsSuppressedExceptions =
  22.                        (IOException[]) suppressedExceptionsField.get(dexPathList);
  23.                if (dexElementsSuppressedExceptions == null) {
  24.                    dexElementsSuppressedExceptions =
  25.                            suppressedExceptions.toArray(
  26.                                    new IOException[suppressedExceptions.size()]);
  27.                } else {
  28.                    IOException[] combined =
  29.                            new IOException[suppressedExceptions.size() +
  30.                                            dexElementsSuppressedExceptions.length];
  31.                    suppressedExceptions.toArray(combined);
  32.                    System.arraycopy(dexElementsSuppressedExceptions, 0, combined,
  33.                            suppressedExceptions.size(), dexElementsSuppressedExceptions.length);
  34.                    dexElementsSuppressedExceptions = combined;
  35.                }
  36.                suppressedExceptionsField.set(dexPathList, dexElementsSuppressedExceptions);
  37.            }
  38.        }
  39.        private static Object[] makeDexElements(
  40.                Object dexPathList, ArrayList<File> files, File optimizedDirectory,
  41.                ArrayList<IOException> suppressedExceptions)
  42.                        throws IllegalAccessException, InvocationTargetException,
  43.                        NoSuchMethodException {
  44.            Method makeDexElements =
  45.                    findMethod(dexPathList, “makeDexElements”, ArrayList.class, File.class,
  46.                            ArrayList.class);
  47.            return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory,
  48.                    suppressedExceptions);
  49.        }
  50.    }

V19与V14差别不大,只不过DexPathList#makeDexElements方法多了一个ArrayList<IOException>参数,如果在执行DexPathList#makeDexElements方法的过程中出现异常,后面使用反射的方式把这些异常记录进DexPathList的dexElementsSuppressedExceptions字段里面。

无论是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文件。)

到这里,MultiDex的工作流程就结束了。怎么样,是不是觉得和以前谈到动态加载技术(插件化)的时候说的很像?没错,谁叫它们的核心都是dex文件呢。Java老师第一节课就说“类就是编程”,搞定类你就能搞定整个世界啊!

优化方案

MultiDex有个比较蛋疼的问题,就是会产生明显的卡顿现象,通过上面的分析,我们知道具体的卡顿产生在解压dex文件以及优化dex两个步骤。不过好在,在Application#attachBaseContext(Context)中,UI线程的阻塞是不会引发ANR的,只不过这段长时间的卡顿(白屏)还是会影响用户体验。

目前,优化方案能想到的有两种。

PreMultiDex方案

大致思路是,在安装一个新的apk的时候,先在Worker线程里做好MultiDex的解压和Optimize工作,安装apk并启动后,直接使用之前Optimize产生的odex文件,这样就可以避免第一次启动时候的Optimize工作。

安装dex的时候,核心是创建DexFile对象并使用其Native方法对dex文件进行opt处理,同时生产一个与dex文件(.zip)同名的已经opt过的dex文件(.dex)。如果安装dex的时候,这个opt过的dex文件已经存在,则跳过这个过程,这会节省许多耗时。所以优化的思路就是,下载Apk完成的时候,预先解压dex文件,并预先触发安装dex文件以生产opt过的dex文件。这样覆盖安装Apk并启动的时候,如果MultiDex能命中解压好的dex和odex文件,则能避开耗时最大的两个操作。

不过这个方案的缺点也是明显的,第一次安装的apk没有作用,而且事先需要使用内置的apk更新功能把新版本的apk文件下载下来后,才能做PreMultiDex工作。

异步MultiDex方案

这种方案也是目前比较流行的Dex手动分包方案,启动App的时候,先显示一个简单的Splash闪屏界面,然后启动Worker线程执行MultiDex#install(Context)工作,就可以避免UI线程阻塞。不过要确保启动以及启动MultiDex#install(Context)所需要的类都在主dex里面(手动分包),而且需要处理好进程同步问题。

参考资料:

著作信息:

本文章出自 Kaede 的博客,原创文章若无特别说明,均遵循 CC BY-NC 4.0 知识共享许可协议4.0(署名-非商用-相同方式共享),可以随意摘抄转载,但必须标明署名及原地址。

未经允许不得转载:JX BLOG » MultiDex工作原理分析和优化方案

赞 (0)

评论 0

评论前必须登录!

登陆 注册