当前位置: 代码迷 >> 综合 >> Android MultiDex 分包及加载原理
  详细解决方案

Android MultiDex 分包及加载原理

热度:48   发布时间:2023-12-15 03:03:26.0

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 15targetSdkVersion 28//启用多DexmultiDexEnabled 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) {
    //读取入参argsif (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=65536Main.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 MultiDexif (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中需要打包的Classesthis.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.xxxxdoInstallation(context,new File(applicationInfo.sourceDir),new File(applicationInfo.dataDir),CODE_CACHE_SECONDARY_FOLDER_NAME,NO_KEY_PREFIX,true);
}private static void doInstallation(...) {
    ...//拿到当前application对应ClassloaderClassLoader 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 的pathListField 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-gradle
https://yangxiaobinhaoshuai.github.io/在这里插入图片描述

  相关解决方案