Android MultiDex 分包及加载原理

699 阅读7分钟

Problem

日常开发中,一旦项目变的庞大起来,很容易遇到如下的编译错误:

    trouble writing output:

    Too many field references: 131000; max is 65536\.

    You may try using \-\-multi\-dex option\.

    //低版本编译会遇到类似这种

    Conversion to Dalvik format failed:

    Unable to execute dex: method ID not in \[0, 0xffff\]: 65536

错误信息也很明确,表示单个Dex文件内可以包含的方法引用数不能超过65536,正好是2的16次方64Kb,有时候也叫“64K引用限制”。

如何规避

遇到以上问题,第一反应当然是精简代码:

  • 检查应用的直接和传递依赖项:简单说就是一个类能解决的问题不要引入一个库,这种也是日常开发中最常见的,很多时候我们为了用到某一个轮子,而引入了一整辆马车。这种可以通过精简一些第三方库、support包等。通过代码压缩、移除未使用的代码:很多代码年久失修,其实可以重构或者删除掉。即使如此,上述策略还是无法彻底解决64K引用的问题,官方提供了将一个Dex拆分为多个Dex的库来越过这一限制,这就是MultiDex。

  • 引入MultiDex MultiDex可以理解为一个工具集,一方面在编译打包时将你的代码从之前的生成一个Classes.dex 变为生成Classes.dex、Classes1.dex...ClassesN.dex多个Dex文件;另一方面它也提供了应用运行时对这多个Dex的加载。

Android 5.0之前版本支持多Dex

Android5.0之前编译版本要支持编译时对Dex进行分包,需要如下配置:

    android {

    defaultConfig {

    minSdkVersion 15

    targetSdkVersion 28

    //启用多Dex

    multiDexEnabled true

    }

    }

    dependencies {

    implementation 'com.android.support:multidex:1.0.3'

    }

Android 5.0之前使用Dalvik执行应用代码,默认情况下,Dalvik限制每个APK只能使用一个Classes.dex,所以要支持运行时多Dex加载,需要配置当前Application类,要么继承MultiDexApplication,要么在当前Application中调用如下方法

    @Override

    protected void attachBaseContext(Context base) {

    super.attachBaseContext(base);

    //运行时多Dex加载, 继承MultiDexApplication最终也是调用这个方法

    MultiDex.install(this);

    }

Android 5.0之后版本支持多Dex

Android 5.0之后的版本使用ART运行时,它本身支持从APK文件中加载多个Dex文件。并且ART在应用安装时执行预编译,会扫描所有的ClassesN.dex, 统一优化为.oat文件。并且编译时如果minSdkVersion>=21, 则默认情况下支持分包,不需要引入上述support库。

综上,Android 5.0之前需要引入对应的support库来支持编译时分包和运行时加载多Dex,而Android 5.0之后由于使用ART虚拟机,运行时本身支持加载多Dex,minSdkVersion >=21 编译期也本身支持分包,因此不必引入相关配置。

MultiDex 分包原理

引入 multiDexEnabledtrue 之后,就可以支持打包生成多个Dex文件,因此,这一过程肯定是在编译期间发生,从官方的打包流程图也可以看出,最终是通过dex工具将class文件转换为Dex文件,

dx实际上是个脚本,其执行对应的jar包路径为 /sdk/build-tools/27.0.x/lib/dx.jar ,我们可以将其导入AndroidStudio,分析其源码:

    //找到对应的入口类

    //com.android.dx.command.Main.java

    public class Main {

    public static void main(String[] args) {

    //读取入参args

    if (arg.equals("--dex")) {

    com.android.dx.command.dexer.Main.main(without(args, i));

    break;

    }

    ...

    }

    }

    //com.android.dx.command.dexer.Main.java

    public static void main(String[] argArray) throws IOException {

    DxContext context = new DxContext();

    //封装入参, Arguments构造函数中指定了maxNumberOfIdxPerDex=65536

    Main.Arguments arguments = new Main.Arguments(context);

    arguments.parse(argArray);

    //执行

    int result = (new Main(context)).runDx(arguments);

    if (result != 0) {

    System.exit(result);

    }

    }

    public int runDx(Main.Arguments arguments) throws IOException {

    //一堆分装参数,初始化IO逻辑

    ...

    int var3;

    try {

    //gradle中enable MultiDex

    if (this.args.multiDex) {

    var3 = this.runMultiDex();

    return var3;

    }

    var3 = this.runMonoDex();

    } finally {

    this.closeOutput(humanOutRaw);

    }

    return var3;

    }

    private int runMultiDex() throws IOException {

    assert !this.args.incremental;

    //看来是去读一个关键文件 mainDexListFile(主Dex相关)

    if (this.args.mainDexListFile != null) {

    // 保存主Dex中需要打包的Classes

    this.classesInMainDex = new HashSet();

    // 从mainDexListFile中读取需要打包在MainDex中的类并保存

    readPathsFromFile(this.args.mainDexListFile, this.classesInMainDex);

    }

    //起一个线程池

    this.dexOutPool =Executors.newFixedThreadPool(this.args.numThreads);

    if (!this.processAllFiles()) {

    return 1;

    } else if (!this.libraryDexBuffers.isEmpty()) {

    throw new DexException("Library dex files are not supported in multi-dex mode");

    } else {

    //提交对应任务,通过DexWriter将Class转化为Dex文件

    if (this.outputDex != null) {

    this.dexOutputFutures.add(this.dexOutPool.submit(new Main.DexWriter(this.outputDex)));

    this.outputDex = null;

    }

    //

    if (this.args.jarOutput) {

    ...

    } else if (this.args.outName != null) {

    File outDir = new File(this.args.outName);

    assert outDir.isDirectory();

    for(int i = 0; i < this.dexOutputArrays.size(); ++i) {

    //getDexFileName(i)==>i == 0 ? "classes.dex" : "classes" + (i + 1) + ".dex";

    FileOutputStream out = new FileOutputStream(new File(outDir, getDexFileName(i)));

    try {

    out.write((byte[])this.dexOutputArrays.get(i));

    } finally {

    this.closeOutput(out);

    }

    }

    }

    return 0;

    }

    }

    //每个提交的任务中对Class进行单独处理,包括进行校验方法引用数等,这里篇幅有限,不再深入,感兴趣的同学自行研究

上面提到的MainDex中的类主要是由mainDexListFile指定的,而mainDexListFile的生成是通过SDK中的 mainDexClasses、mainDexClasses.rules、mainDexClassesNoAapt.rules等相关脚本生成,具体逻辑可以自行研究。

总结一下, MultiDex的分包是在编译期借助dx和mainDexClasses等脚本,确定主Dex(仅包含入口类和引用类)和其他Dex的具体字节码组成,并且生成对应文件的过程,篇幅所限,后续可对照相关源码深入研究。

MultiDex 加载原理

如果对Android ClassLoader比较熟悉的话,其实多Dex加载的原理也比较简单,后续的插件化和热修复也用到了类似思想,以下是源码的一些关键路径分析:

    //MultiDex.java

    public static void install(Context context) {

    //通过context拿到当前application信息

    ...

    //sourceDir: data/app/com..xxxx/base.apk

    //dataDir: data/data/com.xxxx

    doInstallation(context,

    new File(applicationInfo.sourceDir),

    new File(applicationInfo.dataDir),

    CODE_CACHE_SECONDARY_FOLDER_NAME,

    NO_KEY_PREFIX,

    true);

    }

    private static void doInstallation(...) {

    ...

    //拿到当前application对应Classloader

    ClassLoader loader = mainContext.getClassLoader(); //PathClassLoader

    //ClassesN.dex对应释放路径

    File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);

    //将目录下的base.apk解压提取classesN.dex,源码后续分析

    MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir);

    IOException closeException = null;

    try {

    List<? extends File> files =

    extractor.load(mainContext, prefsKeyPrefix, false);

    try {

    //重点代码

    installSecondaryDexes(loader, dexDir, files);

    //容错 Some IOException causes may be fixed by a clean extraction.

    } catch (IOException e) {

    if (!reinstallOnPatchRecoverableException) {

    throw e;

    }

    files = extractor.load(mainContext, prefsKeyPrefix, true);

    installSecondaryDexes(loader, dexDir, files);

    }

    } finally {

    ...

    }

    }

    //

    private static void installSecondaryDexes(ClassLoader loader, File dexDir,List<? extends File> files) {

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

    }

    }

    }

    //以V19为例

    private static final class V19 {

    static void install(ClassLoader loader..) {

    //获取当前ClassLoader 的pathList

    Field pathListField = findField(loader, "pathList");

    Object dexPathList = pathListField.get(loader);

    //通过调用DexPathList.makeDexElements(ArrayList<File> files, File optimizedDirectory); 传入之前释放出来的Classes1.dex...ClassesN.dex所在路径,生成对应的DexElements, 然后和当前已加载主Dex的Classloader对应的DexPathList中的DexElement合并,之后再通过发射设置给当前ClassLoader对应的DexPthList,这样,当前ClassLoader就拥有一个包含所有DexElement的dexPathList,也就可以访问其他多个Dex的

    expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,

    new ArrayList <File> (additionalClassPathEntries), optimizedDirectory,

    suppressedExceptions));

    }

    }

    //反射替换

    private static void expandFieldArray(Object instance, String fieldName,

    Object[] extraElements) {

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

    }

    //构造DexClement[]

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

    }

对照源码可以看出,MultiDex的加载原理比较简单,主要是从ClassLoader入手,通过反射调用使得当前加载了主Dex文件的ClassLoader也可以读取到其他Dex。但我们从中可以看出这里有很多IO操作,容易出现ANR问题,这也决定了我们的分包Dex也不能过大。

MultiDex的局限性

  • 如果分包的Dex过大,上述install过程涉及IO等操作,容易触发ANR问题;

  • 当运行的版本低于 Android 5.0(API 级别 21)时,使用多 dex 文件不足以避开 linearalloc 限制(参考google:https://issuetracker.google.com/issues/37008143)。此上限在 Android 4.0(API 级别 14)中有所提高,但这并未完全解决该问题。在低于 Android 4.0 的版本中,可能会在达到 DEX 索引限制之前达到 linearalloc 限制。因此,如果您的目标 API 级别低于 14,请在这些版本的平台上进行全面测试,因为您的应用可能会在启动时或加载特定类组时出现问题。代码压缩可以减少甚至有可能消除这些问题。

总结

由于Dex文件结构的限制,方法引用数不能超过64K,因此除了努力缩减代码之外,官方也提供了一套工具库,一方面支持编译时分包,一个APK中包含多个Dex,同时也利用ClassLoader原理巧妙的绕过了Dalvik加载APK时只加载一个Dex的限制。而Android 5.0 N之后引入ART,这些问题被巧妙的隐藏或者解决了,但MultiDex的加载原理ClassLoader在后续的热修复插件化等方案中应用的很广泛。

参考资料:

https://developer.android.com/studio/build/multidex#mdex-gradlehttps://yangxiaobinhaoshuai.github.io/