当我们的app越来越大时,project build时可能会失败,报下面的错误。
Conversion to Dalvik format failed:
Unable to execute dex: method ID not in [0, 0xffff]: 65536
或者是这样:
trouble writing output:
Too many field references: 131000; max is 65536.
You may try using --multi-dex option.
上面的错误都是说dex文件中索引id超过了64k范围,只是使用不同版本android编译系统提示不一样而已。通常的理解是dex文件引用的方法数索引使用short类型保存,决定了单个dex包最大的方法数只有64k。具体分析,可以参考由Android 65K方法数限制引发的思考。
Google官方解决方案——Multidex
google为了解决这个问题提出了Multidex,如果工程方法数超过了64k,可以自动将我们的代码和资源拆分成多个dex文件。Android 5.0之前可以使用multidex support library来实现,配置也比较简单。
- build tools版本升到21.1以上,gradle添加multidex依赖,并且启用multidex选项
android {
compileSdkVersion 21
buildToolsVersion "21.1.0"
defaultConfig {
...
minSdkVersion 14
targetSdkVersion 21
...
// Enabling multidex support.
multiDexEnabled true
}
...
}
dependencies {
compile 'com.android.support:multidex:1.0.0'
}
- 使用MultidexApplication
三种方法,按项目需求选择一种即可:
1)可以在manifest中直接指定project Application为MultidexApplication;
2)可以让项目的Application继承MultidexApplication
3)不继承MultidexApplication也可以,直接在Application的attachBaseContext()方法中调用MultiDex.install(this);方法加载multidex
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
Android 5.0之后使用ART(Android Runtime),ART原生支持从apk中加载multidex。应用安装时,ART会扫描apk中所有的dex文件,然后编译成单个oat文件供Android设备运行。目前市场上android 4.4的占有率还是比较高的,因此multidex方案还是有必要的。
另外,应用在启动时,系统会负责帮助我们加载主dex文件,而只有在Application实例化的时候才会调用MultiDex.install(this)方法加载二级以及之后的dex。由于android系统在退出应用时只要不杀app进程,进程会存活一段时间以便下次复用,这样能够加快应用启动速度。因此,只有在应用冷启的时候才会去加载二级以及之后的dex文件。
Multidex方案缺点(Android 4.0以下系统)
(1)Multidex方案是同步加载的,如果二级dex文件较大的话,应用启动时可能会出现ANR。因此,虽然我们可以不用担心方法数超过上限的问题,但是App瘦身工作还是要做的。可以开启ProGuard混淆,帮助我们删除不需要的依赖以及无用的资源和代码,可以帮助我们减小App size。
(2)Android 4.0 之前系统,存在Dalvik linearAlloc bug,Mutidex可能会失效,也就可能发生crash。
(3)开启了Multidex的应用需要分配很大的内存,Android 5.0以下的系统可能会触发Dalvik linearAlloc 的内存限制,从而造成crash。
Multidex安装过程简析
(1)首先判断当前系统VM是否已经支持multidex;如果支持,弃用使用multidex support library手动加载mutidex的方案。
(2)判断当前SDK_INT是否支持multidex,SDK_INT小于4不支持。
(3)判断secondary dex是否已经安装,防止多进程创建时重复执行安装。
(4)删除之前安装时提取的dex文件,dex文件存放目录为:context.getFilesDir()+”/secondary-dexes”
(5)查看是否有dex文件缓存,如果有直接从缓存中取得dex文件列表;如果没有,解析apk提取dex文件列表
/**
* 获取dex文件列表
**/
static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir, boolean forceReload) throws IOException {
Log.i("MultiDex", "MultiDexExtractor.load(" + applicationInfo.sourceDir + ", " + forceReload + ")");
File sourceApk = new File(applicationInfo.sourceDir);
long currentCrc = getZipCrc(sourceApk);
List files;
if(!forceReload && !isModified(context, sourceApk, currentCrc)) {
try {
files = loadExistingExtractions(context, sourceApk, dexDir);
} catch (IOException var9) {
Log.w("MultiDex", "Failed to reload existing extracted secondary dex files, falling back to fresh extraction", var9);
files = performExtractions(sourceApk, dexDir);
putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
}
} else {
Log.i("MultiDex", "Detected that extraction must be performed.");
files = performExtractions(sourceApk, dexDir);
putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
}
Log.i("MultiDex", "load found " + files.size() + " secondary dex files");
return files;
}
dex文件名从classes2.dex开始,每个dex文件解析如果出错有三次重试机会。dex文件解析完成后putStoredApkInfo()方法在SharedPreferences中保存dex的timestamp、crc校验和数量信息。
(6)安装dex文件列表。不同的android版本有不同的实现方式,如下是api19及以上实现。
private static void install(ClassLoader loader, List<File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
Field pathListField = MultiDex.findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
ArrayList suppressedExceptions = new ArrayList();
MultiDex.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory, suppressedExceptions));
...
}
}
// 将secondary dex 路径加入到primary dex路径之后
private static void expandFieldArray(Object instance, String fieldName, Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
Field jlrField = findField(instance, fieldName);
Object[] original = (Object[])((Object[])jlrField.get(instance));
Object[] combined = (Object[])((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);
}
private static Object[] makeDexElements(Object dexPathList, ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
Method makeDexElements = MultiDex.findMethod(dexPathList, "makeDexElements", new Class[]{ArrayList.class, File.class, ArrayList.class});
return (Object[])((Object[])makeDexElements.invoke(dexPathList, new Object[]{files, optimizedDirectory, suppressedExceptions}));
}
从源码中可以看出,dex的安装过程首先利用反射调用DexPathList的makeDexElements方法将secondary dex文件包装成Element对象,之后再利用反射将Element对象插入DexPathList的dexElements对象中,从而达到修改ClassLoader的dexPathList效果,这样ClassLoader就能找到secondary dex中的代码资源了。
美团Dex自动拆包及动态加载
美团的这套机制是在multidex基础上提出的,主要针对multidex在Android 4.0以下系统存在的缺点进行优化和改进,解决应用冷启速度慢甚至是crash的问题以及linearAlloc缺陷和限制的问题。冷启crash是因为MultiDex.install(this)操作比较耗时导致的,因此可以考虑放到异步线程中去加载。同时由于异步带来时间上的不确定性,必须保证主dex文件包含了应用启动以及首页等所需的全部资源,并且确保在使用二级dex文件中资源时,二级dex已经加载完毕。针对linearAlloc缺陷和限制的问题,可以考虑人工干涉控制各个dex大小来规避。
综上,美团这套机制主要需要解决两个问题:
(1)人工干预dex拆分,包括dex内容、dex大小、dex个数
(2)保证使用各个dex之前,dex已经加载完成
参考文档:
官方multidex文档
https://developer.android.com/studio/build/multidex.html#mdex-gradle
美团Android DEX自动拆包及动态加载简介
http://tech.meituan.com/mt-android-auto-split-dex.html