深入浅出ClassLoader加载机制
1.ClassLoader
一个完整的Java程序是由多个.class文件组成的,在程序运行过程中,需要将这些.class文件加载到JVM中才可以使用。而负责加载这些.class文件的就是本类加载器(ClassLoader)。
在 Java 程序启动的时候,并不会一次性加载程序中所有的 .class 文件,而是在程序的运行过程中,动态地加载相应的类到内存中。
Java 中的类 被 ClassLoader 加载时机
- 调用类的构造器
- 调用类中的静态(static)变量或者静态方法
JVM 中自带 3 个类加载器
- 启动类加载器
BootstrapClassLoader
- 扩展类加载器
ExtClassLoader
(JDK1.9之后,改名为PlatformClassLoder
) - 系统加载器
APPClassLoader
APPClassLoader
应用程序类加载器,主要加载系统属性“java.class.path”配置下类文件,也就是环境变量CLASS_PATH配置的路径。因此 AppClassLoader 是面向用户的类加载器,我们自己编写的代码以及使用的第三方 jar 包通常都是由它来加载的。
ExtClassLoader
扩展类加载器 加载系统属性“java.ext.dirs”配置下类文件,可以打印出这个属性来查看具体有哪些文件:
System.out.println(System.getProperty("java.ext.dirs"));
结果如下:
![](https://img.haomeiwen.com/i9513946/9e04f7b54bfaf615.png)
BootstrapClassLoader
启动类加载器,是由 C/C++ 语言实现(其他的类加载器都由Java语言实现),是虚拟机自身的一部分,因此我们无法在 Java 代码中直接获取它的引用。如果尝试在 Java 层获取 BootstrapClassLoader 的引用,系统会返回 null。
BootstrapClassLoader 加载系统属性“sun.boot.class.path”配置下类文件,可以打印出这个属性来查看具体有哪些文件:
System.out.println(System.getProperty("sun.boot.class.path"));
结果如下:
![](https://img.haomeiwen.com/i9513946/093f2dfbe8ec0921.png)
2.双亲委派模型
双亲委派模型(Parents Delegation Model),是指当类加载器收到加载类或资源的请求时,通常都是先委托给父类加载器加载,只有当父类加载器找不到指定类或资源时,自身才会执行实际的类加载过程。类加载器双亲委派模型如下图:
![](https://img.haomeiwen.com/i9513946/1a4274d86fa692b8.png)
双亲委派模型要求除了顶层的启动类加载器以外,其余的类加载器都应当有自己的父类加载器。这里的类加载器之间的父子关系不会以继承(Inheritance)的关系来实现,而是以组合(Composition)的关系来复用父加载器的代码。这样做的意义是为了性能,每次加载都会消耗时间,但如果父加载器加载过,就可以直接拿来用。
其具体实现代码是在 ClassLoader.java 中的 loadClass 方法中,如下所示:
![](https://img.haomeiwen.com/i9513946/bf512d59d6a9ea40.png)
该方法执行如下操作:
- 判断该 Class 是否已加载,如果已加载,则直接将该 Class 返回。
- 如果该 Class 没有被加载过,则判断 parent 是否为空,如果不为空则将加载的任务委托给parent。
- 如果 parent == null,则直接调用 BootstrapClassLoader 加载该类。
- 如果 parent 或者 BootstrapClassLoader 都没有加载成功,则调用当前 ClassLoader 的 findClass 方法继续尝试加载。
上面操作中的 parent 是 ClassLoader 的构造器中传入的一个 CLassLoader 类型的 parent 引用,如果我们继续查看源码,可以看到AppClassLoader 传入的 parent 就是 ExtClassLoader,而 ExtClassLoader 传入的parent为null。
“双亲委派”机制只是Java推荐的机制,并不是强制的机制。我们可以继承java.lang.ClassLoader类,实现自己的类加载器。如果想保持双亲委派模型,就应该重写 findClass(name) 方法;如果想破坏双亲委派模型,可以重写 loadClass(name) 方法。
3.自定义 ClassLoader
JVM中预置的3种ClassLoader只能加载特定目录下的.class文件,如果我们想加载其他特殊位置下的jar包或类时(比如,我要加载网络或者磁盘上的一个.class文件),默认的 ClassLoader 就不能满足我们的需求了,所以需要定义自己的 Classloader 来加载特定目录下的 .class 文件。
步骤:
- 自定义一个类继承抽象类 ClassLoader。
- 重写 findClass 方法。
- 在 findClass 中,调用 defineClass 方法将字节码转换成 Class 对象,并返回。
上述动态加载.class文件的思路,经常被用作热修复和插件化开发的框架中,包括QQ空间热修复方案、微信Tinker等原理都是由此而来。客户端只要从服务端下载一个加密的.class文件,然后然后在本地通过事先定义好的加密方式进行解密,最后再使用自定义 ClassLoader 动态加载解密后的 .class 文件,并动态调用相应的方法。
4.Android 中的 ClassLoader
在Android虚拟机里是无法直接运行.class文件的,Android会将所有的.class文件转换成一个.dex文件,并且Android将加载.dex文件的实现封装在BaseDexClassLoader 中,而我们一般只使用它的两个子类:PathClassLoader 和 DexClassLoader。
PathClassLoader
PathClassLoader 用来加载系统 apk 和被安装到手机中的 apk 内的 dex 文件。它的 2 个构造函数如下:
![](https://img.haomeiwen.com/i9513946/6e628bfac6cb19a1.png)
参数说明:
- dexPath:dex 文件路径,或者包含 dex 文件的 jar 包路径;
- librarySearchPath:C/C++ native 库的路径。
PathClassLoader里面除了这2个构造方法以外就没有其他的代码了,具体的实现都是在BaseDexClassLoader里面,其dexPath比较受限制,一般是已经安装应用的 apk 文件路径。
当一个 App 被安装到手机后,apk 里面的 class.dex 中的 class 均是通过 PathClassLoader 来加载的。
DexClassLoader
对比PathClassLoader只能加载已经安装应用的dex或apk文件,DexClassLoader则没有此限制,可以从SD卡上加载包含class.dex的.jar 和 .apk 文件,这也是插件化和热修复的基础,在不需要安装应用的情况下,完成需要使用的 dex 的加载。
DexClassLoader 的源码里面只有一个构造方法,代码如下:
![](https://img.haomeiwen.com/i9513946/6926fa6e7ff7f335.png)
参数说明:
- dexPath:包含 class.dex 的 apk、jar 文件路径 ,多个路径用文件分隔符(默认是“:”)分隔。
- optimizedDirectory:用来缓存优化的 dex 文件的路径,即从 apk 或 jar 文件中提取出来的 dex 文件。该路径不可以为空,且应该是应用私有的,有读写权限的路径。
对于APP而言,Apk文件中有一个class.dex文件,这个dex就是Apk的主dex,是通过PathClassLoader加载的。在App的Activity中,通过getClassLoader方法获取到的是PathClassLoader,它的父类是BootClassLoader。
对于插件化而言,有一种方案是将App的ClassLoader替换为自定义的ClassLoader,这样就要求自定义的ClassLoader模拟双亲委派模型。比较典型的代码就是Zeus插件化框架。
5.案例:使用 DexClassLoader 加载外部dex
加载外部dex主要流程如下:
- 从服务器下载插件apk到手机SDCard
- 读取插件apk中的dex,生成对应的DexClassLoader
- 使用DexClassLoader的loadClass方法读取插件dex中的类
我们这里的demo直接把插件apk放到主App的assets目录中,App启动后,再把assets目录中的插件apk复制到内存,通过这种方式来模拟从服务器下载插件。
放进主App的assets目录中的 app-debug.apk 是一个插件,里面有一个Bean类
public class Bean {
private String name = "JokerWan";
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
在主App里面加载 app-debug.apk
- 把assets目录下的 app-debug.apk 插件复制到 /data/data/files 目录下,这部分逻辑封装到Utils里面完成
public static void extractAssets(Context context, String sourceName) {
AssetManager am = context.getAssets();
InputStream is = null;
FileOutputStream fos = null;
try {
is = am.open(sourceName);
File extractFile = context.getFileStreamPath(sourceName);
fos = new FileOutputStream(extractFile);
byte[] buffer = new byte[1024];
int count = 0;
while ((count = is.read(buffer)) > 0) {
fos.write(buffer, 0, count);
}
fos.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
closeSilently(is);
closeSilently(fos);
}
}
- 在App启动的时候调用这个方法,在这个demo中,我重写了MainActivity的attachBaseContext方法,在里面做这个事情
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(newBase);
try {
Utils.extractAssets(newBase, apkName);
} catch (Throwable e) {
e.printStackTrace();
}
}
- 加载插件 app-debug.apk 中的 dex 生成 DexClassLoader
File extractFile = this.getFileStreamPath(apkName);
dexpath = extractFile.getPath();
fileRelease = getDir("dex", Context.MODE_PRIVATE);
classLoader = new DexClassLoader(dexpath,
fileRelease.getAbsolutePath(), null, getClassLoader());
- 调用生成的classLoader的loadClass方法加载app-debug.apk 中的Bean类,并调用其getName方法
Class loadClassBean;
try {
loadClassBean = classLoader.loadClass("com.jokerwan.plugin.Bean");
Object beanObject = loadClassBean.newInstance();
Method getNameMethod = loadClassBean.getMethod("getName");
getNameMethod.setAccessible(true);
String name = (String) getNameMethod.invoke(beanObject);
Toast.makeText(getApplicationContext(), name, Toast.LENGTH_LONG).show();
} catch (Exception e) {
Log.e("JokerWan", "msg:" + e.getMessage());
}
虽然拿到了这个Bean类,但是因为主App中并没有这个Bean类,所以我们只能用反射来实例化Bean并调用它的getName方法。此时,我们就成功的加载了外部插件apk,并可以获取到该插件的所有类。
6.总结
- ClassLoader就是用来加载class文件的,不管是jar中还是dex中的class。
- Java 中的 ClassLoader 通过双亲委派模型来加载各自指定路径下的 class 文件。
- 可以自定义 ClassLoader,一般覆盖 findClass() 方法,不建议重写 loadClass 方法。
- Android 中常用的两种 ClassLoader 分别为:PathClassLoader 和 DexClassLoader。PathClassLoader 用来加载系统 apk 和被安装到手机中的 apk 内的 dex 文件;DexClassLoader可以从SD卡上加载包含class.dex的.jar 和 .apk 文件。