文章目录
Android ClassLoader介绍
什么是class loader,对于Android来说,class loader主要做两件事情
- 加载dex文件
- 根据class path加载并返回对应的Class<?>对象
BaseDexClassLoader
Android类加载的基础实现类其实是BaseDexClassLoader,看其构造函数
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
- dexPath 要加载的dex文件列表,多个文件路径用 : 分割
- optimizedDirectory 存放dex优化后的odex文件的目录
- libraryPath 库路径,多个也是用 :分割,如果app没有native library,则为null
- parent 父class loader,用于双亲委派(parent delegation)
在构造函数内,基于如上参数创建DexPathList,DexPathList内部主要做了:
- 调用makeDexElements,根据上面提供的dex文件列表依次创建DexFile和Element,并保存到数组
BaseDexClassLoader创建完成后,就可以调用loadClass按如下顺序加载对应的class了
- 如果parent delegation不为null,尝试调用parent.loadClass(name, false)获取
- 接着调用self.findClass
- 继续调用this.pathList.findClass, pathList会依次从dexElements获取DexFile来尝试获取class,如果成功就返回
PathClassLoader和DexClassLoader
这两个类都派生子BaseDexClassLoader,有什么区别?看构造函数就知道了
//PathClassLoader constructor
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
//DexClassLoader constructor
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
二者主要区别是,而DexClassLoader可以设置optimizedDirectory,而PathClassLoader不能,必须使用默认的,也就是系统默认路径/data/dalvik-cache
所以,DexClassLoader能加载并optimize任意的dex文件,PathClassLoader则用于加载系统和已安装app的dex文件
为什么要支持MultiDex?
看了上面的介绍,我们知道android class loader默认是支持从多个dexes文件列表中依次尝试加载class类的,为什么要支持multi dex加载呢?
这个是历史遗留问题
Unable to execute dex: method ID not in [0, 0xffff]: 65536
Conversion to Dalvik format failed: Unable to execute dex: method ID not in [0, 0xffff]: 65536
Android VM在加载并解析dex文件后,在执行method时所需的某个变量(具体我也不清楚)类型为short,也就是16bit, 这就限制了dex包含的method数量为65536,超过就不行了(是不是觉得好坑?你只能说近十来年,硬件的进化远远超出了当时设计者的想象)
随着app功能越来越多,生成的dex method总数终归是要超过64k的,所以,为了解决这个限制,必须要将生成的dex做拆分,这就是class loader支持MultiDex的原因
MultiDex打包
Dex打包拆分目前通用的解决方案是使用官方的multidex support library
defaultConfig {
...
// Enabling multidex support.
multiDexEnabled true
}
添加依赖
implementation com.android.support:multidex:1.0.2
将multiDexEnabled开关打开,这样在编译打包时,如果生成的dex method数量超过65536,会自动将其拆分,然后打包到apk的根目录
- classes.dex
- classes1.dex
- classes(1…N).dex
其中classes.dex是main dex,其他的是secondary dexes,很明显,classes.dex会作为入口dex被加载,这里又产生一个疑问,在编译时,如何确定哪些类会被打入classes.dex?
在Android sdk build tool目录下,会存在mainDexClasses脚本,用于配置哪些类需要被打入classes.dex
下面是build-tools/22.0.1/mainDexClasses.rules的内容
-keep public class * extends android.app.Instrumentation {
<init>();
}
-keep public class * extends android.app.Application {
<init>();
void attachBaseContext(android.content.Context);
}
-keep public class * extends android.app.Activity {
<init>();
}
-keep public class * extends android.app.Service {
<init>();
}
-keep public class * extends android.content.ContentProvider {
<init>();
}
-keep public class * extends android.content.BroadcastReceiver {
<init>();
}
-keep public class * extends android.app.backup.BackupAgent {
<init>();
}
# We need to keep all annotation classes because proguard does not trace annotation attribute
# it just filter the annotation attributes according to annotation classes it already kept.
-keep public class * extends java.lang.annotation.Annotation {
*;
}
很明显,四大组件和Application等都被打入了main dex
安装加载
在5.0系统之后,Android系统在安装apk时,就会对MultiDex做合成并放置到
/data/dalvik-cache/apk***@classes.dex
这样App启动时就不需要再做MultiDex装载,这在一定程度上会提高App的启动速度
不过在5.0系统以前,如果apk包含多dex,在安装完成后dex分布如下
- classes.dex main dex文件放置于/data/dalvik-cache/apk***@classes.dex
- secondary dexes会被放置于 /data/data/package/code_cache/secondary-dexes/下
然后在apk启动时,通过在调用MultiDex.install(this)来完成对secondary dexes的加载
public class HelloMultiDexApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
}
在MultiDex.install(this)成功之前,任何对secondary-dexes内类的访问,都会触发class not found exception
接着简单看下MultiDex.install(this)实现原理
public static void install(Context context) {
Log.i("MultiDex", "Installing application");
if (IS_VM_MULTIDEX_CAPABLE) {
Log.i("MultiDex", "VM has multidex support, MultiDex support library is disabled.");
} else if (VERSION.SDK_INT < 4) {
throw new RuntimeException("MultiDex installation failed. SDK " + VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + ".");
} else {
try {
ApplicationInfo applicationInfo = getApplicationInfo(context);
if (applicationInfo == null) {
Log.i("MultiDex", "No ApplicationInfo available, i.e. running on a test Context: MultiDex support library is disabled.");
return;
}
doInstallation(context, new File(applicationInfo.sourceDir), new File(applicationInfo.dataDir), "secondary-dexes", "");
} catch (Exception var2) {
Log.e("MultiDex", "MultiDex installation failure", var2);
throw new RuntimeException("MultiDex installation failed (" + var2.getMessage() + ").");
}
Log.i("MultiDex", "install done");
}
}
doInstallation主要调用getDexDir拿到/data/data/package/code_cache目录,接着调用installSecondaryDexes安装该目录下的所有dex,原理也很简单,最终就是通过反射拿到当前class loader的pathList(DexPathList),接着调用其静态函数makeDexElements根据如上dex dir目录下dex list创建Element数组,最后将其追加到pathList已有Element数组
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);
}
参考文档
Android拆分与加载Dex的多种方案对比
預防 Android Dex 64k Method Size Limit