我们在上一篇文章中,学习到Java中的
ClassLoader的加载顺序以及双亲委托机制。但是在Android中的ClassLoader又有点不一样,Android重写了整个ClassLoader。我们来了解一下Android的ClassLoader机制
概述
Java的虚拟机是JVM,Android虽说是基于JAVA,但是为了更适应手机的特性,Android使用了自己特有的Dalvik/ART虚拟机。
虽说是另一个虚拟机,但是ClassLoader的机制依旧存在,而且相似,Android的ClassLoader一样有特定的加载顺序和双亲委托机制。
Dalvik/ART 虚拟机同样依靠ClassLoader来加载对应的类,但是不同于Java,Android在打包apk时并不是直接把class文件打包,而是对class文件优化之后生成dex文件,Android将所有的class文件打包成一个或多个(multiDex)文件。
然后在安装App时,Android虚拟机会进一步对apk中的dex文件进行优化:
Dalivk虚拟机会使用DexOpt提取apk中的dex文件进一步优化,生成一个ODEX文件存储在缓存路径(
/data/dalvik-cache/)下,而后打开APP可以直接加载ODEX文件而不用解析apk而ART虚拟机则会将apk中的dex文件优化为机器指令,保存为OAT文件于缓存路径下(
/data/dalvik-cache/),不同于ODEX文件,CPU不需要再去解析OAT文件,因为里面已经是机器指令,这样的机制大大提高了运行效率,不过相对的占用空间就变大了
Android特有的ClassLoader
ClassLoader
Android重写了ClassLoader,我们先来看一下ClassLoader的重点代码:
public abstract class ClassLoader {
ClassLoader(ClassLoader parentLoader, boolean nullAllowed) {
// ……省略
parent = parentLoader;
}
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
// 省略部分代码
// 查找已经加载的类
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
// 委托parent加载
clazz = parent.loadClass(className, false);
if (clazz == null) {
// 自己加载,空方法,交由子类实现
clazz = findClass(className);
}
}
return clazz;
}
}
可以看到ClassLoader同样是拥有parent和双亲委托原则,逻辑基本和Java的一样。
不过可以看到Android中废弃了Java中将jar文件转换为Class的方法defineClass,而一般子类会将该过程交由JNI实现。
BootClassLoader
不过,我们可以看到ClassLoader文件下还有另一个类:
/**
* 位于其他ClassLoader的顶层,内部基于JNI实现
*/
class BootClassLoader extends ClassLoader {
private static BootClassLoader instance;
public static synchronized BootClassLoader getInstance() {
if (instance == null) {
instance = new BootClassLoader();
}
return instance;
}
public BootClassLoader() {
// parent置为null
super(null, true);
}
@Override
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
Class<?> clazz = findLoadedClass(className);
// 因为parent为null,所以跳过了调用parent.loadClass这一步
if (clazz == null) {
clazz = findClass(className);
}
return clazz;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 通过JNI实现
return Class.classForName(name, false, null);
}
}
看这个名字,我们立刻就联想到JVM中的BootStrapClassLoader,没错,这个ClassLoader正是位于其他ClassLoader的顶层,也就是虚拟机第一个加载的ClassLoader,同样这个类的实际实现是基于JNI的。
该类负责加载Android的核心类库,如String,Activity等。
BaseDexClassLoader
看完这个类,我们再来看看ClassLoader的子类:BaseDexClassLoader:
/*
* 解析Dex文件的ClassLoader的基类
*/
public class BaseDexClassLoader extends ClassLoader {
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
super(parent);
this.originalPath = dexPath;
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//……省略,主要基于JNI
}
}
这里提取了只提取了其中重要部分的代码,可以看到他的构造函数传入了dexPath等参数,而findClass方法主要基于JNI,正如他的注释所说,这是findClass通过JNI解析路径下的dex文件。
我们来重点看一下他的参数:
-
dexPath
待解析文件所在的全路径,classloader将在该路径中指定的dex文件寻找指定目标类 -
optimzedDirectory
优化路径,指的是虚拟机对于apk中的dex文件进行优化后生成文件存放的路径,如dalvik虚拟机生成的ODEX文件路径和ART虚拟机生成的OAT文件路径。
这个路径必须是当前app的内部存储路径,Google认为如果放在公有的路径下,存在被恶意注入的危险 -
libraryPath
指定native层代码存放路径 -
parent
当前ClassLoader的parent,和java中classloader的parent含义一样
我们前面说了,Dalvik/ART虚拟机在第一次安装apk时,会对dex文件进行优化,存放到缓存路径,后续是直接读取缓存路径下的文件,而不再读取原文件,这里的optimzedDirectory正是指的优化后的缓存路径。
所以BaseDexClassLoader在loadClass会执行的一个流程大致如下(因为底层的JNI实现所以这里不看源码了,有兴趣可自行了解,这里只说结论):
- 判断
optimzedDirectory路径下是否有对应的优化过的文件(ODEX/OAT) - 如果步骤1判断否,那么解析
dexPath路径指定的dex文件,进行优化并存储到optimzedDirectory路径下,否则直接进入步骤3 - 读取
optimzedDirectory路径下对应的文件 - 解析为
Class
PathDexClassLoader
接下我们看一下BaseDexClassLoader的子类,他有两个子类,DexClassLoader和PathClassLoader,我们分别看一下:
/**
* 提供一个简单的ClassLoader去加载路径下指定的dex/jar/apk文件
* Android系统通过该ClassLoader去加载系统应用类和App应用
* Android建议我们不应该使用该类去加载我们自定义的类而是使用
* DexClassLoader
*/
public class PathClassLoader extends BaseDexClassLoader {
/**
* @param dexPath 指定的dex/jar/apk文件的路径,可以包含多个路径,
* 用{File.pathSeparator}分割。
* @param parent
*/
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
// 与上一个构造函数类似,只是多了一个libraryPath表示Native库路径
public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
}
可以看到,PathClassLoader里除了构造函数没有其他方法,所以根本的逻辑还是基于BaseDexClassLoader来完成。
特殊的一点是,我们发现PathClassLoader指定了optimzedDirectory为null??
这是为什么?
这个问题需要我们进入JNI才能解答,这里贴上Native层解析class的一段注释:

这段注释的意思是,如果输入的optimzedDirectory为空,那么会使用默认的cache路径,也就是我们刚才提到的/data/dalvik-cache/。但是我们要注意到一点,一般情况下,我们的App对于这个文件夹是没有读写权限的,因此我们也就没有办法使用PathClassLoader去加载自定义的类。正如注释说的这个类一般由系统调用加载系统类和App应用。也就是说我们App中的类MainActivity等都是由其加载。
DexClassLoader
而我们如果要加载自定义的类应该使用DexClassLoader,也就是BaseDexClassLoader的另一个子类:
/**
* 一个用于加载路径下指定的dex/jar/apk文件的ClasLoader
* 可以加载沒有安装过的APK
* 这个ClassLoader需要一个应用内私有,且可写入的路径去存储优化后的dex文件(optimizedDirectory)
* 不要将优化后的文件存储在外部存储区,因为这将有可能导致你的App被恶意注入
*/
public class DexClassLoader extends BaseDexClassLoader {
/**
* @param dexPath 指定的dex/jar/apk文件的路径,可以包含多个路径,
* 用{File.pathSeparator}分割。
* @param optimizedDirectory 储存优化后文件的路径,必须是可写入的,不能为null
* @param libraryPath 表示Native库路径
* @param parent the parent class loader
*/
public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
可以看到,DexClassLoader和PathClassLoader类似,都只是重写了构造方法,我们可以看到,其实只是对于optimizedDirectory转换成File而已。这也真是他和PathClassLoader的不同之处,他可以自定义optimizedDirectory,我们可以指向一个我们有访问权限的文件,所以我们可以利用他来加载自定义的类。
总结
- Android重写了Java层的
ClassLoader,延续了parent和双亲委派机制 - Android中同样的
ClassLoader同样有一个最顶层的parent,不过不同于Java中用JNI实现,在Android中是Java实现的BootClassLoader,该类负责加载Android的核心类库 -
BaseDexClassLoader,封装了一些列解析dex/jar/apk文件方法的基类。其在loadClass的时候会将dex文件解析并优化到optimizedDirectory路径下,再进行解析。 -
PathClassLoader,其实他的位置有点想Java中的AppClassLoader,Android系统会通过他来加载系统应用类和App类。由于没有权限访问他的文件夹,所以不适用于我们加载自定义类,一般用于加载已经安装的Apk等 -
DexClassLoader,用于加载自定义的类,包括没有安装过的APK也可以加载,需要指定optimizedDirectory来存储优化dex后的文件。
