MultiDex原理分析


单个Dex文件,即Dalvik Executable,代码中可调用的引用总数最多为64K(65536个).

版本差异

  • Android 5.0 之前

    使用的是Dalvik虚拟机,默认情况下Dalvik会限制每个APK只能使用一个classes.dex字节码文件.

    为了绕过这一限制,可以使用multidex库,进行分dex.

    在运行时,multidex会使用特殊的ClassLoader搜索对应方法的的所有dex文件,而不只是classes.dex.

  • Android 5.0 及更高版本

    使用ART虚拟机,支持从APK加载多个Dex文件.

    ART虚拟机在APP安装时会进行预编译,会扫描classesN.dex文件,并将它们编译成.oat文件,因此不需要使用multidex.

一般把classes.dex,叫做primary dex,或者主要dex.

指定主要dex中必须包含的类

android {
    buildTypes {
        release {
            multiDexKeepFile file('multidex-config.txt')
            ...
        }
    }
}

multidex-config.txt中的格式如下:

com/example/MyClass.class
com/example/MyOtherClass.class

还有一个multiDexKeepProguard经常与上面的multiDexKeepFile搭配使用.

源码分析

我们使用的基本上都是MultiDex.install(context):

public static void install(Context context) {
    // 判断当前虚拟机是否已经支持了多个dex文件,比如如果是ART虚拟机,java.vm.version 的majorversion >=2
    if (IS_VM_MULTIDEX_CAPABLE) {
        Log.i(TAG, "VM has multidex support, MultiDex support library is disabled.");
        return;
    }
    ApplicationInfo applicationInfo = getApplicationInfo(context);
    doInstallation(context,
                new File(applicationInfo.sourceDir), // base.apk
                new File(applicationInfo.dataDir),   // /data/data/packageName/
                CODE_CACHE_SECONDARY_FOLDER_NAME,    // "secondary-dexes"
                NO_KEY_PREFIX,                       // ""
                true);
}

来看一下具体的安装过程:

    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);

            // Android 5.0及以上版本,log提示不需要
            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") + "\"");
            }

            // 可用用来加载Dex字节码的ClassLoader,multidex会修改它的pathList属性以添加额外的Dex文件
            // 这里返回的ClassLoder一定是BaseDexClassLoader或者它的子类,因为只有他们才可以加载Dex字节码
            ClassLoader loader = getDexClassloader(mainContext);
            // 清理/data/data/packageName/secondary-dexes/下的文件
            clearOldDexDir(mainContext);
            // 创建并获取/data/data/packageName/code_cache/secondary-dexes/
            File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);

            // 会创建一个Multidex.lock的空文件,用于文件锁,防止该操作与dexopt并行执行
            MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir);
            // 加载dex文件,把次要的dex每个都写入zip中
            List<? extends File> files =
                        extractor.load(mainContext, prefsKeyPrefix, false);
            // 关键,加载次要dex
            installSecondaryDexes(loader, dexDir, files);
        }
    }

这里面其实就是解压APK,从APK中获取到Dex,并且把次要dex都打包成zip,后续处理:

    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) {
                // >= Android 4.4
                V19.install(loader, files, dexDir);
            } else if (Build.VERSION.SDK_INT >= 14) {
                // Androd 4.0 <= x < Android 4.4
                V14.install(loader, files);
            } else {
                // Android 4.0以下
                V4.install(loader, files);
            }
        }
    }

看一下Android 4.4以上的逻辑:

        static void install(ClassLoader loader,
                List<? extends File> additionalClassPathEntries,
                File optimizedDirectory)
                        throws IllegalArgumentException, IllegalAccessException,
                        NoSuchFieldException, InvocationTargetException, NoSuchMethodException,
                        IOException {

            // 找到BaseDexClassLoader子类的pathList Field 
            // 这里会从当前classloader找,找不到就找父类的              
            Field pathListField = findField(loader, "pathList");
            Object dexPathList = pathListField.get(loader);

            ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
            //关键:将次要dex添加到dexElements中              
            expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
                    new ArrayList<File>(additionalClassPathEntries), 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);
    }

    // 使用makeDexElements,将次要的dex添加到elmentents中
    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 Element[] makeDexElements(List<File> files, File optimizedDirectory,
            List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
      Element[] elements = new Element[files.size()];
      int elementsPos = 0;

      for (File file : files) {
          if (file.isDirectory()) {
              elements[elementsPos++] = new Element(file);
          } else if (file.isFile()) {
              String name = file.getName();
              // 加载Dex,构造DexFile对象
              DexFile dex = null;
              dex = loadDexFile(file, optimizedDirectory, loader, elements);
              // 将dex添加到elemnts中
              if (dex == null) {
                  elements[elementsPos++] = new Element(file);
              } else {
                  elements[elementsPos++] = new Element(dex, file);
              }

              if (dex != null && isTrusted) {
                dex.setTrusted();
              }
          }
      }
      if (elementsPos != elements.length) {
          elements = Arrays.copyOf(elements, elementsPos);
      }
      return elements;
    }

    // 调用Native方法打开Dex文件,构造DexFile对象
    private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader,
                                       Element[] elements)
            throws IOException {
        if (optimizedDirectory == null) {
            return new DexFile(file, loader, elements);
        } else {
            String optimizedPath = optimizedPathFor(file, optimizedDirectory);
            return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);
        }
    }

这里主要做的就是Hook BaseDexClassLoader,找到其中的pathList字段,并将dex封装到Element中,添加到dexPathList的elements中.

总结

multidex的关键其实就是利用反射机制,Hook BaseDexClassLoader及其子类中的pathList,将除了主要dex的其他dex都利用classloader加载到pathList中.


文章作者: 姜康
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 姜康 !
评论
 上一篇
Android Studio查看和调试AOSP源码 Android Studio查看和调试AOSP源码
之前说过使用VSCode阅读AOSP源码的方法,但是作为Android开发,还是对Android Studio熟悉一些,这里看下如何使用Android Studio查看AOSP源码. 如果你之前没完整的编译过AOSP,可以按照下面的流程进行
2020-08-21
下一篇 
Android APP启动流程分析 Android APP启动流程分析
之前也写过Application启动流程之类的文章,但是总感觉这个程度不够,再来总结下. 先把脑子里那一堆忘掉,想一想要启动一个Application需要干什么: 需要知道app可执行文件的位置 即apk中的dex文件,或者经过dexop
2020-08-14
  目录