日常开发中,一旦项目变的庞大起来,很容易遇到如下的编译错误:
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引用限制”。
遇到以上问题,第一反应当然是精简代码:
即使如此,上述策略还是无法彻底解决64K引用的问题,官方提供了将一个Dex拆分为多个Dex的库来越过这一限制,这就是MultiDex。
MultiDex可以理解为一个工具集,一方面在编译打包时将你的代码从之前的生成一个Classes.dex 变为生成Classes.dex、Classes1.dex…ClassesN.dex多个Dex文件;另一方面它也提供了应用运行时对这多个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之后的版本使用ART运行时,它本身支持从APK文件中加载多个Dex文件。并且ART在应用安装时执行预编译,会扫描所有的ClassesN.dex, 统一优化为.oat文件。并且编译时如果minSdkVersion>=21, 则默认情况下支持分包,不需要引入上述support库。
综上,Android 5.0之前需要引入对应的support库来支持编译时分包和运行时加载多Dex,而Android 5.0之后由于使用ART虚拟机,运行时本身支持加载多Dex,minSdkVersion >=21 编译期也本身支持分包,因此不必引入相关配置。
引入 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的具体字节码组成,并且生成对应文件的过程,篇幅所限,后续可对照相关源码深入研究。
如果对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 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也不能过大。
如果分包的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/