理解ClassLoader加载机制

1.什么是ClassLoader

我们知道java中的.java文件在运行前是需要编译成.class文件,然后由JVM加载这些class文件,而负责这个加载过程的就是ClassLoader

1.1 类加载时机

通常情况下,以下两种情况ClassLoader会主动加载class文件

  1. 调用类构造器
  2. 调用静态变量或者静态方法
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 步骤
  1. 自定义类继承ClassLoader
  2. 重写findClass方法
  3. 在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 创建补丁包
  1. 导出class文件

修复一下原来出bug的SomeException类

public class SomeException implements IDoSomething {
    @Override
    public String doSomethingResult() {
         return "bug已修复";
    }
}

然后build一下项目,然后找到编译之后的class文件,路径如下图,将整个包复制到桌面,然后只保留我们修复完的SomeException.class文件

在这里插入图片描述
注意:复制出来后将项目中SomeException类改回原来出bug的状态。

  1. 使用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.总结

注意事项

  1. 修复方法需要在bug类之前执行。
  2. 导出class文件时需要连同包名一起导出。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值