一个完整的 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部分源码APPClassLoader 主要加载 java.class.path 路径的 class 文件,而这个路径的文件是面向用户编写的代码的加载器。我们编写的代码,以及引用的第三方 jar 包,都是通过这个类加载器进行加载。
ExtClassLoader 扩展类加载器
ExtClassLoader 部分源码ExtClassLoader 加载器主要加载 java.ext.dirs 路径下的class文件,这个路径加载 jdk 子文件夹下的 ext 资源文件。
ExtClassLoader 加载的文件路径
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,代码如下:
Secret.java
测试类所在磁盘路径如下图:
接下来,创建 DiskClassLoader 继承 ClassLoader,重写 findClass 方法,并在其中调用 defineClass 创建 Class,代码如下:
DiskClassLoader.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 个构造函数如下:
PathClassLoader 构造方法参数说明:
-
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 类,如下:
最后运行效果如下:
网友评论