一个完整的 Java 程序是由多个 .class 文件组成的,在程序运行过程中,需要将这些 .class 文件加载到 JVM 中才可以使用。而负责加载这些 .class 文件的就是类加载器(ClassLoader)。
Java中的类什么时候被加载器加载
在Java中,一般不会一次性加载完所有的.class文件,而是在运行过程中,动态加载到内存中。
主动被加载到内存的时机一般有两种情况:
调用类构造器
调用类中的静态(static)变量或者静态方法
Java中的ClassLoader
Java中有三种ClassLoader加载器:
启动类加载器 BootstrapClassLoader
扩展类加载器 ExtClassLoader (JDK 1.9 之后,改名为 PlatformClassLoader)
系统加载器 APPClassLoader
以上 3 者在 JVM 中有各自分工,但是又互相有依赖。
APPClassLoader
APPClassLoader 主要加载 java.class.path 路径的 class 文件,而这个路径的文件是面向用户编写的代码的加载器。我们编写的代码,以及引用的第三方 jar 包,都是通过这个类加载器进行加载。
ExtClassLoader 扩展类加载器
ExtClassLoader 加载器主要加载 java.ext.dirs 路径下的class文件,这个路径加载 jdk 子文件夹下的 ext 资源文件。
BootstrapClassLoader
BootstrapClassLoader 同上面的两种 ClassLoader 不太一样。
首先,它并不是使用 Java 代码实现的,而是由 C/C++ 语言编写的,它本身属于虚拟机的一部分。因此我们无法在 Java 代码中直接获取它的引用。如果尝试在 Java 层获取 BootstrapClassLoader 的引用,系统会返回 null。
BootstrapClassLoader 加载系统属性“sun.boot.class.path”配置下类文件,可以打印出这个属性来查看具体有哪些文件:
全是 JRE 目录下的 jar 包或者 .class 文件。
双亲委派模式
既然 JVM 中已经有了这 3 种 ClassLoader,那么 JVM 又是如何知道该使用哪一个类加载器去加载相应的类呢?答案就是:双亲委派模式。
所谓双亲委派模式就是,当类加载器收到加载类或资源的请求时,通常都是先委托给父类加载器加载,也就是说,只有当父类加载器找不到指定类或资源时,自身才会执行实际的类加载过程。
APPClassLoader 的父类加载器就是 ExtClassLoader 。
注意:“双亲委派”机制只是 Java 推荐的机制,并不是强制的机制。我们可以继承 java.lang.ClassLoader 类,实现自己的类加载器。如果想保持双亲委派模型,就应该重写 findClass(name) 方法;如果想破坏双亲委派模型,可以重写 loadClass(name) 方法。
自定义 ClassLoader
虽然JVM预置了这三种加载器,但是只能加载特定路径下的class文件。然而在实际项目中,可能会有需求加载特定的路径,如加载网络或者磁盘上的一个 .class 文件。这种情况就需要自己定义加载器来加载特定目录下的 .class 文件。
自定义 ClassLoader 步骤
自定义一个类继承抽象类 ClassLoader。
重写 findClass 方法。
在 findClass 中,调用 defineClass 方法将字节码转换成 Class 对象,并返回。
用一段伪代码来描述这段过程如下:
自定义实践
首先在本地电脑上创建一个测试类 Secret.java,代码如下:
测试类所在磁盘路径如下图:
接下来,创建 DiskClassLoader 继承 ClassLoader,重写 findClass 方法,并在其中调用 defineClass 创建 Class,代码如下:
最后,写一个测试自定义 DiskClassLoader 的测试类,用来验证我们自定义的 DiskClassLoader 是否能正常 work。
解释说明:
① 代表需要动态加载的 class 的路径。
② 代表需要动态加载的类名。
③ 代表需要动态调用的方法名称。
最后执行上述 testClassLoader 方法,并打印如下结果,说明我们自定义的 DiskClassLoader 可以正常工作。
注意:上述动态加载 .class 文件的思路,经常被用作热修复和插件化开发的框架中,包括 QQ 空间热修复方案、微信 Tink 等原理都是由此而来。客户端只要从服务端下载一个加密的 .class 文件,然后在本地通过事先定义好的加密方式进行解密,最后再使用自定义 ClassLoader 动态加载解密后的 .class 文件,并动态调用相应的方法。
Andrid中的ClassLoader
本质上,Android 和传统的 JVM 是一样的,也需要通过 ClassLoader 将目标类加载到内存,类加载器之间也符合双亲委派模型。但是在 Android 中, ClassLoader 的加载细节有略微的差别。
在 Android 虚拟机里是无法直接运行 .class 文件的,Android 会将所有的 .class 文件转换成一个 .dex 文件,并且 Android 将加载 .dex 文件的实现封装在 BaseDexClassLoader 中,而我们一般只使用它的两个子类:PathClassLoader 和 DexClassLoader。
PathClassLoader
PathClassLoader 用来加载系统 apk 和被安装到手机中的 apk 内的 dex 文件。它的 2 个构造函数如下:
参数说明:
dexPath:dex 文件路径,或者包含 dex 文件的 jar 包路径;
librarySearchPath:C/C++ native 库的路径。
PathClassLoader 里面除了这 2 个构造方法以外就没有其他的代码了,具体的实现都是在 BaseDexClassLoader 里面,其 dexPath 比较受限制,一般是已经安装应用的 apk 文件路径。
当一个 App 被安装到手机后,apk 里面的 class.dex 中的 class 均是通过 PathClassLoader 来加载的,可以通过如下代码验证:
打印结果如下:
DexClassLoader
先看官方对 DexClassLoader 的描述:
A class loader that loads classes from .jar and .apk filescontaining a classes.dex entry.
This can be used to execute code notinstalled as part of an application.
很明显,对比 PathClassLoader 只能加载已经安装应用的 dex 或 apk 文件,DexClassLoader 则没有此限制,可以从 SD 卡上加载包含 class.dex 的 .jar 和 .apk 文件,这也是 插件化 和 热修复 的基础,在不需要安装应用的情况下,完成需要使用的 dex 的加载。
DexClassLoader 的源码里面只有一个构造方法,代码如下:
参数说明:
dexPath:包含 class.dex 的 apk、jar 文件路径 ,多个路径用文件分隔符(默认是“:”)分隔。
optimizedDirectory:用来缓存优化的 dex 文件的路径,即从 apk 或 jar 文件中提取出来的 dex 文件。该路径不可以为空,且应该是应用私有的,有读写权限的路径。
使用 DexClassLoader 实现热修复
创建 Android 项目 DexClassLoaderHotFix
项目结构如下:
ISay.java 是一个接口,内部只定义了一个方法 saySomething。
SayException.java 实现了 ISay 接口,但是在 saySomething 方法中,打印“something wrong here”来模拟一个线上的 bug。
最后在 MainActivity.java 中,当点击 Button 的时候,将 saySomething 返回的内容通过 Toast 显示在屏幕上。
最后运行效果如下:
创建 HotFix patch 包
新建 Java 项目,并分别创建两个文件 ISay.java 和 SayHotFix.java。
ISay 接口的包名和类名必须和 Android 项目中保持一致。SayHotFix 实现 ISay 接口,并在 saySomething 中返回了新的结果,用来模拟 bug 修复后的结果。
将 ISay.java 和 SayHotFix.java 打包成 say_something.jar,然后通过 dx 工具将生成的 say_something.jar 包中的 class 文件优化为 dex 文件。
dx --dex --output=say_something_hotfix.jar say_something.jar
上述 say_something_hotfix.jar 就是我们最终需要用作 hotfix 的 jar 包。
将 HotFix patch 包拷贝到 SD 卡主目录,并使用 DexClassLoader 加载 SD 卡中的 ISay 接口
修改 MainActivity 中的逻辑,使用 DexClassLoader 加载 HotFix patch 中的 SayHotFix 类,如下:
最后运行效果如下: