当一个app的功能越来越复杂,代码量越来越多,也许有一天便会突然遇到下列现象:
生成的apk在2.3以前的机器无法安装,提示INSTALL_FAILED_DEXOPT
方法数量过多,编译时出错,提示:
Conversion to Dalvik format failed:Unable to execute dex: method ID not in [0, 0xffff]: 65536
出现这种问题的原因是:
Android2.3及以前版本用来执行dexopt(用于优化dex文件)的内存只分配了5M
一个dex文件最多只支持65536个方法。
解决这个问题可以使用dex分包方案
简单来说,其原理是将编译好的class文件拆分打包成两个dex,绕过dex方法数量的限制以及安装时的检查,在运行时再动态加载第二个dex文件中,除了第一个dex文件(即正常apk包唯一包含的Dex文件),其它dex文件都以资源的方式放在安装包中,并在Application的onCreate回调中被注入到系统的ClassLoader。因此,对于那些在注入之前已经引用到的类(以及它们所在的jar),必须放入第一个Dex文件中。
在Android中,我们采用DexClassLoader来加载未安装的类,实际上实现功能是在BaseDexClassLoader里面,我们看它的源码
private final String originalPath;
/** structured lists of path elements */
private final DexPathList pathList;
final class DexPathList {
private static final String DEX_SUFFIX = ".dex";
private static final String JAR_SUFFIX = ".jar";
private static final String ZIP_SUFFIX = ".zip";
private static final String APK_SUFFIX = ".apk";
/** class definition context */
private final ClassLoader definingContext;
/** dex元素的列表 */
private final Element[] dexElements;
/** list of native library directory elements */
private final File[] nativeLibraryDirectories;
当我们需要装载一个类的时候,实际上是去遍历dex中的Element数组,然后依次去加载所需要的class文件,直到找到为止。
这样我们的解决方案为:将第二个dex文件放入Element数组中,那么在加载第二个dex包中的类时,应该可以直接找到。
下面看看代码实现:
public class BaseApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
}
public String inject(String libPath){
//判断是否有BaseDexClassLoader
boolean hasBaseClassLoader = true;
try {
Class.forName("dalvik.system.BaseDexClassLoader");
} catch (ClassNotFoundException e) {
hasBaseClassLoader = false;
e.printStackTrace();
}
if (hasBaseClassLoader) {
//获取PathClassLoader
PathClassLoader pathClassLoader = (PathClassLoader)getClassLoader();
//获取DexClassLoader
DexClassLoader dexClassLoader = new DexClassLoader(libPath, getDir("dex", 0).getAbsolutePath(), libPath,getClassLoader());
try {
/*
通过反射获取PathClassLoader中的DexPathList中的Element数组(已加载了第一个dex包,由系统加载),
以及DexClassLoader中的DexPathList中的Element数组(刚将第二个dex包加载进去),将两个数组合并之后
再将其赋值给PathClassLoader的Element数组,到此,注入完毕。
*/
Object dexElements = combineArray(getDexElements(getPathList(pathClassLoader)), getDexElements(getPathList(dexClassLoader)));
//获取PathClassLoader中的集合
Object pathList = getPathList(pathClassLoader);
//将合并后的dexElements数组赋值给PathClassLoader的Element数组
setField(pathList, pathList.getClass(), "dexElements", dexElements);
return "SUCCESS";
} catch (Throwable e) {
e.printStackTrace();
return android.util.Log.getStackTraceString(e);
}
}
return "SUCCESS";
}
}
但是使用dex分包方案仍然有几个注意点:
由于第二个dex包是在Application的onCreate中动态注入的,如果dex包过大,会使app的启动速度变慢,因此,在dex分包过程中一定要注意,第二个dex包不宜过大。
由于上述第一点的限制,假如我们的app越来越臃肿和庞大,往往会采取dex分包方案和插件化方案配合使用,将一些非核心独立功能做成插件加载,核心功能再分包加载。
本文探讨了Android应用因Dex方法数量超过65536导致的问题,并介绍了一种解决方案——Dex分包。文章详细解释了Dex分包的工作原理及其实现步骤。
1910

被折叠的 条评论
为什么被折叠?



