Android-浅析-ClassLoader

前言

Linus Benedict Torvalds : RTFSC – Read The Funning Source Code

概述

Classloader 类加载器,用来加载Java类到 Java 虚拟机中的一种加载器。Java程序(class文件)并不是本地的可执行程序。当运行Java程序时,首先运行JVM(Java虚拟机),然后再把Java class加载到JVM里头运行,负责加载Java class的这部分就叫做Class Loader。

Java ClassLoader

我们先从Java的ClassLoader开始学习。

Java语言系统自带有三个类加载器:

  1. Bootstrap ClassLoader : 最顶层的加载类,主要加载核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。
  2. Extention ClassLoader : 扩展的类加载器,加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。还可以加载-D java.ext.dirs选项指定的目录。
  3. Appclass Loader : 也称为SystemAppClass 加载当前应用的classpath的所有类。

这三个类的继承关系如下:
ClassLoader 继承图

加载顺序

Jvm 启动的加载顺序是 Bootstrap ClassLoader->Extention ClassLoader->Appclass Loader。

首先 Bootstrap ClassLoader 是在Jvm刚起动的时候去加载java核心API的。

然后 ExtClassLoader 加载扩展API。

最后 AppClassLoader 加载CLASSPATH目录下定义的Class。

父加载器

每个类加载器都有一个父加载器。这个也跟之后的双亲委托模型相关。

通过 ClassLoader cl = Test.class.getClassLoader();我们可以获取关于这个类的ClassLoader。
一般的用户类的ClassLoader都是由AppClassLoader。而AppClassLoader的父加载器则是ExtClassLoader。但ExtClassLoader的父加载器则是null,这并不是因为它没有父加载器,而是因为父加载器是Bootstrap ClassLoader,Bootstrap ClassLoader是由C/C++编写的,它本身是虚拟机的一部分,所以它并不是一个JAVA类,所以想要直接获取都会拿到null。

如果我们看 int.class 和 string.class 的父加载器,都会是获取为null。

父加载器的实现主要在创建 ClassLoader 对象的时候,有两种方法:

  1. 外部类创建 ClassLoader 时指定一个 ClassLoader 为其父加载器。
  2. 如果没有指定父加载器,则默认的父加载器一律为 AppClassLoader。

而 AppClassLoader 的父加载器是 ExtClassLoader 主要是因为在创建时就指定好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public Launcher() {
ClassLoader extcl;
try {
extcl = ExtClassLoader.getExtClassLoader();
} catch (IOException e) {
}
// Now create the class loader to use to launch the application
try {
// 将ExtClassLoader对象实例传递进去
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
}
}

双亲委托

一个类加载器查找 class 和 resource 时,通过“委托模式”进行的,它首先判断这个 class 是不是在缓存里已经加载成功,如果没有的话它并不是自己进行查找,而是先通过父加载器,然后递归下去,直到Bootstrap ClassLoader,如果Bootstrap classloader找到了,直接返回,如果没有找到,则一级一级返回,最后到达自身去查找这些对象。

通过双亲委托的方式,极大可能地节省加载资源,只要父亲节点加载过该类,其他子节点就不用再进行加载,从而在内存中就只有一份字节码。这样的机制也可以提升安全性。当某些恶意人员企图通过在其 ClassLoader 中修改系统 Class 文件时,就会因为双亲委派机制而无法加载。

ClassLoader双亲委托

ps.JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的。

核心流程

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException 核心的加载方法从 loadclass 为入口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检测是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//父加载器不为空则调用父加载器的loadClass
c = parent.loadClass(name, false);
} else {
//父加载器为空则调用Bootstrap Classloader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {}
if (c == null) {
// If still not found, then invoke findClass in order to find the class.
long t1 = System.nanoTime();
//父加载器没有找到,则调用findclass
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
//调用resolveClass()
resolveClass(c);
}
return c;
}
}

从代码上可以看出双亲委托的主要流程,也可以看到当获取父加载器为null的时候,系统会默认为我们指定到 Bootstrap ClassLoader 来加载。

Android ClassLoader

通过学习 JVM 的 ClassLoader ,我们大致了解了 JVM 的加载,现在可以开始了解下 Android 的 ClassLoader。

Android 的 ClassLoader 跟 JVM 的 ClassLoader 原理上一致,但最终实现不同。

Android 的虚拟机是 Davlik 或者 ART,具体执行的不是 Class 文件,而是针对移动设备进行优化后的 DEX 文件。当然 Android 系统也提供了类似的加载机制,主要是由父类是BaseDexClassLoader的两个子类PathClassLoaderDexClassLoader来完成。

加载顺序

Android Apk启动的加载顺序是 BootClassLoader->PathClassLoader\DexClassLoader。

首先 BootClassLoader 是在Android刚起动的时候去加载一些系统Framework层级需要的类。

然后 PathClassLoader\DexClassLoader 加载Apk的应用类。

结构

下图是一张 Android ClassLoader 的类图:
Android ClassLoader

从图中可以了解到android 的类加载器总共有四个:BootClassLoader,URLClassLoader,PathClassLoader,DexClassLoader

Android 和 JVM 的类加载不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
}
/**
* Creates a new class loader using the specified parent class loader for
* delegation.
*/
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
/**
* Creates a new class loader using the ClassLoader returned by
* the method #getSystemClassLoader() getSystemClassLoader()# as the parent class loader.
*/
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
/**
* Encapsulates the set of parallel capable loader types.
*/
private static ClassLoader createSystemClassLoader() {
String classPath = System.getProperty("java.class.path", ".");
String librarySearchPath = System.getProperty("java.library.path", "");
// TODO Make this a java.net.URLClassLoader once we have those?
return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());
}

从上面的代码可以发现,无论在构造的时候传不传父加载器,android的ClassLoader都必须要有一个父加载器。Android在默认无父加载器传入的情况下,默认父加载器为PathClassLoader且此PathClassLoader父加载器为BootClassLoader

BootClassLoader

BootClassLoader 是 ClassLoader 内部类,由java代码实现而不是c++实现,是Android平台上所有 ClassLoader 的最终 parent。

URLClassLoader

只能用于加载jar文件,但是由于 dalvik 不能直接识别jar,所以在 Android 中无法使用这个加载器。

BaseDexClassLoader

PathClassLoader 和 DexClassLoader 都继承自 BaseDexClassLoader,其中的主要逻辑都是在BaseDexClassLoader完成。

其主要的构造函数如下:

1
2
3
4
5
public class BaseDexClassLoader extends ClassLoader {
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) {
throw new RuntimeException("Stub!");
}
}

主要的四个参数含义:

参数 含义
dexPath String: 包含类和资源的jar和apk文件列表,由File.pathSeparator分隔,在Android上默认为“:”。
optimizedDirectory File:此参数已弃用,不起作用
librarySearchPath String: 包含native libraries的目录列表,由File.pathSeparator分隔;可能为null。
parent ClassLoader: 父加载器。

DexClassLoader

DexClassLoader支持加载APK、DEX和JAR,也可以从SD卡进行加载。因为实际调用的是BaseDexClassLoader方法,而BaseDexClassLoader里对”.jar”,”.zip”,”.apk”,”.dex”后缀的文件都会生成一个对应的dex文件,所以DexClassLoader支持加载jar而URLClassLoader不能。

PathClassLoader

PathClassLoader用来加载Android系统类和应用的类,并且不建议开发者使用。在dalvik虚拟机上,PathClassLoader只能用来加载已安装apk的dex。而在art虚拟机上PathClassLoader可以加载未安装的apk的dex,但还是不建议用,有坑。

双亲委托

Android 的加载和 java 的加载函数不一样,JVM中ClassLoader通过defineClass方法加载jar里面的Class,而Android中是loadClass方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
}
}
return c;
}

可以看到这里是很典型的跟jvm一样的双亲委托模型。
具体流程如下:

  1. 先查询当前ClassLoader实例是否加载过此类,有就返回。
  2. 没有查询到,就查询Parent中是否已经加载过此类,如果已经加载过,就直接返回Parent加载的类。
  3. 如果继承路线上的ClassLoader都没有加载,才由Child执行类的加载工作。

并且同样的,每个层级的类都被当作是不同的类,杜绝了冒充核心类库的问题。

简单实现

首先我们随便创建一个工程:test.apk

1
2
3
4
5
public class Test {
public static String TestHelloWorld() {
return "Test";
}
}

然后在我们的主工程里面调用它:

1
2
3
4
5
6
7
8
9
10
11
12
13
String apkPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/test.apk";
File dexFile = getDir("dex", Context.MODE_PRIVATE);
DexClassLoader classLoader = new DexClassLoader(apkPath, dexFile.getAbsolutePath(), null, getClassLoader());
try {
Class pluginClass = classLoader.loadClass("com.plugin.Test.Test");
Object object = pluginClass.newInstance();
Method method = pluginClass.getDeclaredMethod("TestHelloWorld", null);
Log.d("class_loader", method.invoke(object, null).toString());
} catch (Exception e) {
e.printStackTrace();
}

总结

无论是Android 还是 Java 的classloader都比较简单,分析起来不复杂,但是要运用到工程中的话还是会有相应的难度,在android 中最大的难度是解决三点:1. 资源访问;2. 四大组件生命周期;3. 插件的管理。这几个问题解决好才能真正在Android上运用。