1.什么是ClassLoader
我们知道java中的.java文件在运行前是需要编译成.class文件,然后由JVM加载这些class文件,而负责这个加载过程的就是ClassLoader
1.1 类加载时机
通常情况下,以下两种情况ClassLoader会主动加载class文件
- 调用类构造器
- 调用静态变量或者静态方法
1.2 Java中的ClassLoader
1.2.1 应用加载器 APPClassLoader
主要用于加载系统属性“java.class.path”配置下的类文件,我们自己写的代码和第三方jar包都是由系统加载器进行加载
1.2.2 扩展类加载器 ExtClassLoader(JDK 1.8 之后为PlatformClassLoader)
主要用于加载系统属性“java.ext.dirs”配置下的类文件
1.2.3 启动类加载器 BootstrapClassLoader
启动类加载器不是由Java实现的,而是由C/C++编写的,在java层无法获取到它的引用,主要加载"sun.boot.class.path0配置下文件"
1.3 双亲委派模式
所谓双亲委派模式是指在类加载器收到加载请求时,首先是委托父类加载器去进行加载,如果父类加载器找不到指定的类或资源时,自身才会去执行加载过程。
源码实现
protected Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
synchronized(this.getClassLoadingLock(var1)) {
Class var4 = this.findLoadedClass(var1);//若类已经加载,直接返回该class
if (var4 == null) {
long var5 = System.nanoTime();
try {
if (this.parent != null) {
var4 = this.parent.loadClass(var1, false);//若parent不为null,委托父加载器进行加载
} else {
var4 = this.findBootstrapClassOrNull(var1);//若parent为null,则调用BootstrapClassLoader加载该类
}
} catch (ClassNotFoundException var10) {
}
if (var4 == null) {
long var7 = System.nanoTime();
var4 = this.findClass(var1);//若parent和BootstrapClassLoader均未加载成,则调用当前ClassLoader的findClass()尝试加载
PerfCounter.getParentDelegationTime().addTime(var7 - var5);
PerfCounter.getFindClassTime().addElapsedTimeFrom(var7);
PerfCounter.getFindClasses().increment();
}
}
if (var2) {
this.resolveClass(var4);
}
return var4;
}
}
那么这个parent是什么呢?查看源码
protected ClassLoader(ClassLoader parent){
this(checkClassLoader(),parent);
}
可以看出,在每一个 ClassLoader 中都有一个 CLassLoader 类型的 parent 引用,并且在构造器中传入值。如果我们继续查看源码,可以看到 AppClassLoader 传入的 parent 就是 ExtClassLoader,而 ExtClassLoader 并没有传入任何 parent,也就是 null,那么它会将加载任务委派给BootstrapClassLoader。如果BootstrapClassLoader在jdk/lib目录下无法找到对应的类,则返回null,因此AppClassLoader会调用自己findClass()加载类。
例子
public class Test {
public static void main(String [] ar){
Test c = new Test();
System.out.println("the ClassLoader of Test:" + c.getClass().getClassLoader());
System.out.println("the parent is:" + c.getClass().getClassLoader().getParent());
System.out.println("boot_strap is:" + c.getClass().getClassLoader().getParent()
.getParent());
}
}
双亲委派模式并不是强制要求,我们也可以尝试自定义ClassLoader。
2.自定义ClassLoader
2.1 步骤
- 自定义类继承ClassLoader
- 重写findClass方法
- 在findClass中,调用defineClass方法将字节码转为Class对象并返回
2.2 实践
首先在本地电脑上创建需要加载的测试类
public class UserInfo {
public void getName(){
System.out.println("username is Mike");
}
}
创建自定义ClassLoader继承自ClassLaoder,重写findClass方法,在该方法中调用defineClass将字节码转换为Class对象并返回
public class MyClassLoader extends ClassLoader {
private String path;
public MyClassLoader(String path){
this.path = path;
}
@Override
protected Class<?> findClass(String name) {
String filePath = path + name.replace(".","/") + ".class";
byte [] bytes = null;
Path path = null;
try {
path = Paths.get(new URI(filePath));
bytes = Files.readAllBytes(path);
}catch (Exception e){
e.printStackTrace();
}
return defineClass(name,bytes,0,bytes.length);
}
}
测试
public static void main(String [] ar){
MyClassLoader myClassLoader = new MyClassLoader("file:///Project/demo/src/");
try {
Class cl = myClassLoader.findClass("brins.demo.UserInfo");
if (cl != null){
Object obj = cl.newInstance();
//通过反射调用
Method method = cl.getDeclaredMethod("getName");
method.invoke(obj,null);
}
}catch (Exception e){
}
}
打印结果
3.Android中的ClassLoader
Android虚拟机是无法直接运行.class文件,会将所有class文件转化为dex文件,而Android中dex文件的加载逻辑封装在BaseDexClassLoader中,它有两个子类:PathClassLoader和DexClassLoader
3.1 PathClassLoader
用来加载系统apk以及用户安装的apk内的dex文件
构造函数
参数说明
- dexPath:dex文件路径
- librarySearchPath:native库路径
PathClassLoader的dexPath受限制,一般是已安装应用的apk文件路径
代码验证
打印结果
3.2 DexClassLoader
DexClassLoader的dex加载路径没有限制,可从外部存储中加载dex文件或apk文件,这也是实现热修复的基础,在不安装应用的基础上,完成dex加载。
构造函数
参数说明
- dexPath : dex文件路径,多个文件用文件分隔符 ”:“分隔
- optimizedDirectory:缓存dex文件,一般为应用私有路径
4.使用DexClassLoader实现热修复
4.1 创建项目
结构如图
这里定义了一个接口以及一个具体的实现类来模拟线上bug
public class SomeException implements IDoSomething {
@Override
public String doSomethingResult() {
return "Some Exceptions occur";
}
}
运行结果
4.2 创建补丁包
- 导出class文件
修复一下原来出bug的SomeException类
public class SomeException implements IDoSomething {
@Override
public String doSomethingResult() {
return "bug已修复";
}
}
然后build一下项目,然后找到编译之后的class文件,路径如下图,将整个包复制到桌面,然后只保留我们修复完的SomeException.class文件
注意:复制出来后将项目中SomeException类改回原来出bug的状态。
-
使用dex转换工具将class文件转为dex文件
转换工具路径
配置一下环境变量,然后打开命令行,输入指令:dx --dex --output=文件输出路径 源文件路径
例如,我复制出的class在桌面的dex文件夹中
在目标文件夹中生成了我们想要的dex文件
将classes2.dex复制到手机外部存储,/storage/emulated/0/aa/
3.导入dex
在SplashActivity中导入dex文件
private void readDex() {
if (FixDexUtil.isGoingToFix(this)) {
FixDexUtil.loadFixedDex(this, new File(Environment.getExternalStorageDirectory(), "aa"));
}
}
isGoingToFix会根据文件夹中是否有dex文件判断是否进行热修复
看下效果
这样我们通过热修复做到了在不更新app的情况下,修复了bug
源码地址
https://github.com/BrinsLee/classloader.git
5.总结
注意事项
- 修复方法需要在bug类之前执行。
- 导出class文件时需要连同包名一起导出。