我们在上一篇文章中,学习到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后的文件。