Android---深入理解ClassLoader的加载机制

目录

Java 中的 ClassLoader

1. APPClassLoader 系统类加载器

2. ExtClassLoader 扩展类加载器

3. BootstrapClassLoader 启动类加载器

双亲委派模式(Parents Delegation Model)

Android 中的 ClassLoader

1. PathClassLoader

2. DexClassLoader

总结


一个完整的 Java 程序是由多个 .class 文件组成的,在程序运行的过程中,需要将这些 .class 文件加载到 JVM 中才可以使用,而负责加载这些 .class 文件的就是类加载器(ClassLoader)

Java 中的类何时被类加载器加载

Java 程序启动时,并不会一次性加载程序中所有的 .class 文件,而是在程序运行过程中,动态的加载相应的类到内存中。通常情况下,Java 程序中的 .class 文件会在以下两种情况下被 Class Loader 主动加载到内存:

1. 调用类构造器;

2. 调用类中的静态变量或者静态方法。

Java 中的 ClassLoader

JVM 中自带3个类加载器:

\bullet 启动类加载器 BootstrapClassLoader;

\bullet 扩展类加载器 ExtClassLoader(JDK1.9 之后,改名为 PlatformClassLoader);

\bullet 系统类加载器 APPClassLoader。

1. AppClassLoader 系统类加载器

部分源码如下

Android---深入理解ClassLoader的加载机制_第1张图片

可以看出 AppClassLoader 主要加载系统属性“java.class.path”配置下类文件,也就是环境变量 classpath 配置的路径下的文件。因此,AppClassLoader 是面向用户的类加载器。我们自己编写的代码以及使用的第三方 jar 包,通常都是由它来加载的。

2. ExtClassLoader 扩展类加载器

部分源码如下:

Android---深入理解ClassLoader的加载机制_第2张图片

可以看出,扩展类加载器(ExtClassLoader )主要加载系统属性“java.ext.dirs”配置下类文件,可以通过如下代码打印出这个属性来查看具体有哪些文件。

System.out.println(System.getProperty("java.ext.dirs"));

3. BootstrapClassLoader 启动类加载器

BootstrapClassLoader 是由 C/C++ 语言编写的,它本身属于虚拟机的一部分,因此无法在 java 代码中直接获取它的引用。如果尝试在 Java 层获取 BootstrapClassLoader 的引用,系统会返回 null。

启动类加载器加载“sun.boot.class.path”配置下类文件,可以打印这个属性来查看具体有哪些文件

System.out.println(System.getProperty("sun.boot.class.path"));

结果如下:

Android---深入理解ClassLoader的加载机制_第3张图片

可以看出这些全身 jre 目录下的的 jar 包或者 classes 文件。

双亲委派模式(Parents Delegation Model)

既然 JVM 中已经有了这3种 ClassLoader,那么 JVM 又是如何知道该使用哪一个类加载器去加载相应的类呢?答案这是:双亲委派模式。

双亲委派模式:当类加载器受到加载类或资源的请求时,通常都是先委托给父类加载器加载,只有当父类加载器找不到指定的类或资源时,自身才会执行实际的类加载过程。

Android---深入理解ClassLoader的加载机制_第4张图片

其具体实现代码是在CLassLoader.java 中的 loadClass 方法中,如上图所示:

解释说明:上图中1处判断该 class 是否已经加载。如果已经加载,直接将该 class 返回;2处,如果该 class 没有被加载过,则判断 parent 是否为空,如果不为空则将加载的任务委托给 parent;3处,如果 parent 为空,则直接调用 BootstrapClass Loader 加载该类;4处,如果 parent 和 BootstrapClassLoader 都没有加载成功,则调用当前 ClassLoader 的 findClass() 方法继续尝试加载。

那么这个 parent 是什么呢?我们可以看 ClassLoader 的构造器,如下:

可以看出,在每一额 ClassLoader 中都有一个 ClassLoader 类型的 parent 引用,并且在构造器中传入值。继续查看源码,可以看到 AppClassLoader 传入的 parent 就是 ExtClassLoadr,而 ExtClassLoadr并没有传入任何 parent,也就是 null。

举例说明:

Test test = new Test();

默认情况下,JVM 首先调用 AppClassLoader 去加载 Test 类

1. APPClassLoader 将加载任务委派给它的父类加载器(parent)--ExtClassLoader;

2. ExtClassLoader 的 parent 为 null,则直接调用 BootstrapClassLoader 加载该类;

3. BootstrapClassLoader 在 jdk/jre 目录下无法找到 Test 类,因此返回的 Class 为 null;

4. 因为 parent 和 BootstrapClassLoader 都没能成功加载 Test 类,所以 AppClassLoader 会调用自身的 findClass() 方法来加载 Test 类。

最终,Test 就是被系统类加载器(AppClassLoader)加载到内存中的。

注意:“双亲委派”机制只是 Java 推荐的机制,并不是强制的机制。可以继承 java.lang.ClassLoader 类,实现自己的类加载器。如果想保持双亲委派模型,应该重写 findClass(name) 方法;如果想破坏双亲委派模型,可以重写 loadClass(name)方法。

自定义 ClassLoader

JVM 中预置的3种 ClassLoader 只能加载特定目录下的 .class 文件。如果想加载其它特殊位置下的 jar 包或类时(如网络或磁盘上),默认的 ClassLoader 就不能满足我们的需求。所以需要定义自己的 ClassLoader 来加载特定目录下的 .class 文件。

自定义 ClassLoader 步骤

1. 自定义一个类(MyClassLoader)继承抽象类 ClassLoader;

2. 重写 findClass 方法;

3. 在 findClass 方法中,调用 defineClass() 方法将字节码转换成 Class 对象,并返回。

自定义 ClassLoder 实践

1. 创建一个测试类 Secret.java,实现简单的打印功能。该文件存放在 D:\HL\ 目录下。在终端通过 javac 命令生成 Secret.class 文件。

public class Secret {
	public void printSecret() {
		System.out.println("I am a girl!");
	}
}

2. 创建 MyClassLoade 继承自 ClassLoader,重写 findClass() 方法。

public class MyClassLoader extends ClassLoader{
	
	private String filePath;
	
	public MyClassLoader(String path) {
		filePath = path;
	}
	
	@Override
	protected Class findClass(String name){
		// newPath = "D:\HL\Secret.class"
		// File.separator 来构建文件路径,以确保在不同操作系统上都能正常工作
		String newPath = filePath + File.separator + name.replace('.', File.separatorChar) + ".class";
		byte[] classBytes = null;
		
		try {
			Path path = Paths.get(newPath);
			classBytes = Files.readAllBytes(path);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		// 调用 defineClass 创建 class 并返回
		return defineClass(name, classBytes, 0, classBytes.length);
	}

}

3. 测试

public class TestMyClassLoader {

	public static void main(String[] args) {
		// 创建自定义 ClassLoader 对象 
		// "D:/HL/" : 需要动态加载的 class 的路径
		MyClassLoader myClassLoader = new MyClassLoader("D:/HL/");
		try {
			//"software_test.Secret" 需要动态加载的类名
			Class c = myClassLoader.loadClass("software_test.Secret");
			if(c != null) {
				Object obj = c.newInstance();
				// 通过反射调用 Secret 的 printSecret 方法,即需要动态调用的方法名称
				Method method = c.getDeclaredMethod("printSecret", null);
				method.invoke(obj, null);
			}
					
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

}

Android 中的 ClassLoader

本质上 Android 和传统的 JVM 是一样的,也需要 ClassLoader 将目标类加载到内存,类加载器之间也复合双亲委派模型。但是在 Android 中,ClassLoader的加载细节有略微的差别。在 Android 虚拟机里是无法直接运行 .class 文件,Android 中会将所有的 .class 文件转化为一个 .dex 文件。Android 将加载 .dex 文件的实现封装在 BaseDexClassLoader 中,一般我们使用它的两个子类:PathClassLoader 和 DexClassLoader。

1. PathClassLoader

用来加载系统 apk 和被安装到手机中 apk 内的 dex 文件。它的两个构造函数如下:

Android---深入理解ClassLoader的加载机制_第5张图片

参数说明:

dexPath:dex 文件路径,或者包含 dex 文件的 jar 包路径;

librarySearchpath:C/C++ native 库路径。

PathClassLoader 代码里除了这两个构造函数外就没有其它代码了。具体的实现都是在 BaseDexClassLoader 里面。

当一个 app 被安装到手机后,apk 里面的 class.dex 中的 .class 均是通过 PathClassLoader 来加载的。

2. DexClassLoader

对比 PathClassLoader 只能加载已经安装的应用的 dex 或 apk 文件,DexClassLoader 则没有此限制,可以从 SD 卡上加载包含 class.dex 的 jar 包或者是 apk 文件。这也是插件化或热修复的基础,在不需要安装应用的情况下完成需要使用的 dex 文件的加载。

DexClassLoader 的源码里只有一个构造函数,如下:

参数说明:

dexPath:包含 class.dex 的 apk,jar 文件路径,多个路径使用文件分割符(默认是“:”)分隔;

 optimizedDirectory:用来缓存优化的 dex 文件的路径,即从 apk 或 jar 文件中提取出来的 dex 文件,该路径不可以为空,且应该是私有的,有读写权限的路径。

总结

\bullet ClassLoader 是用来加载 class 文件的,不管是 jar 中还是 dex 中的 class;

\bullet Java 中的 ClassLoader 通过双亲委托来加载各自指定路径下的 class 文件;

\bullet 可以自定义 ClassLoader,一般覆盖 findClass() 方法,不建议重写 loadClass 方法;

\bullet Android 中常用的两个 ClassLoader 分别为:PathClassLoader 和 DexClassLoader。

你可能感兴趣的:(#,Android进阶,android)