Android进阶之路

深入浅出ClassLoader加载机制

2020-04-25  本文已影响0人  Joker_Wan

1.ClassLoader

一个完整的Java程序是由多个.class文件组成的,在程序运行过程中,需要将这些.class文件加载到JVM中才可以使用。而负责加载这些.class文件的就是本类加载器(ClassLoader)。

在 Java 程序启动的时候,并不会一次性加载程序中所有的 .class 文件,而是在程序的运行过程中,动态地加载相应的类到内存中。

Java 中的类 被 ClassLoader 加载时机

  1. 调用类的构造器
  2. 调用类中的静态(static)变量或者静态方法

JVM 中自带 3 个类加载器

  1. 启动类加载器BootstrapClassLoader
  2. 扩展类加载器ExtClassLoader(JDK1.9之后,改名为PlatformClassLoder
  3. 系统加载器APPClassLoader

APPClassLoader

应用程序类加载器,主要加载系统属性“java.class.path”配置下类文件,也就是环境变量CLASS_PATH配置的路径。因此 AppClassLoader 是面向用户的类加载器,我们自己编写的代码以及使用的第三方 jar 包通常都是由它来加载的。

ExtClassLoader

扩展类加载器 加载系统属性“java.ext.dirs”配置下类文件,可以打印出这个属性来查看具体有哪些文件:

System.out.println(System.getProperty("java.ext.dirs"));

结果如下:


BootstrapClassLoader

启动类加载器,是由 C/C++ 语言实现(其他的类加载器都由Java语言实现),是虚拟机自身的一部分,因此我们无法在 Java 代码中直接获取它的引用。如果尝试在 Java 层获取 BootstrapClassLoader 的引用,系统会返回 null。

BootstrapClassLoader 加载系统属性“sun.boot.class.path”配置下类文件,可以打印出这个属性来查看具体有哪些文件:

System.out.println(System.getProperty("sun.boot.class.path"));

结果如下:


2.双亲委派模型

双亲委派模型(Parents Delegation Model),是指当类加载器收到加载类或资源的请求时,通常都是先委托给父类加载器加载,只有当父类加载器找不到指定类或资源时,自身才会执行实际的类加载过程。类加载器双亲委派模型如下图:

双亲委派模型要求除了顶层的启动类加载器以外,其余的类加载器都应当有自己的父类加载器。这里的类加载器之间的父子关系不会以继承(Inheritance)的关系来实现,而是以组合(Composition)的关系来复用父加载器的代码。这样做的意义是为了性能,每次加载都会消耗时间,但如果父加载器加载过,就可以直接拿来用。

其具体实现代码是在 ClassLoader.java 中的 loadClass 方法中,如下所示:


该方法执行如下操作:

  1. 判断该 Class 是否已加载,如果已加载,则直接将该 Class 返回。
  2. 如果该 Class 没有被加载过,则判断 parent 是否为空,如果不为空则将加载的任务委托给parent。
  3. 如果 parent == null,则直接调用 BootstrapClassLoader 加载该类。
  4. 如果 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 文件。

步骤:

  1. 自定义一个类继承抽象类 ClassLoader。
  2. 重写 findClass 方法。
  3. 在 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 个构造函数如下:


参数说明:

PathClassLoader里面除了这2个构造方法以外就没有其他的代码了,具体的实现都是在BaseDexClassLoader里面,其dexPath比较受限制,一般是已经安装应用的 apk 文件路径。

当一个 App 被安装到手机后,apk 里面的 class.dex 中的 class 均是通过 PathClassLoader 来加载的。

DexClassLoader

对比PathClassLoader只能加载已经安装应用的dex或apk文件,DexClassLoader则没有此限制,可以从SD卡上加载包含class.dex的.jar 和 .apk 文件,这也是插件化和热修复的基础,在不需要安装应用的情况下,完成需要使用的 dex 的加载。

DexClassLoader 的源码里面只有一个构造方法,代码如下:


参数说明:

对于APP而言,Apk文件中有一个class.dex文件,这个dex就是Apk的主dex,是通过PathClassLoader加载的。在App的Activity中,通过getClassLoader方法获取到的是PathClassLoader,它的父类是BootClassLoader。

对于插件化而言,有一种方案是将App的ClassLoader替换为自定义的ClassLoader,这样就要求自定义的ClassLoader模拟双亲委派模型。比较典型的代码就是Zeus插件化框架。

5.案例:使用 DexClassLoader 加载外部dex

加载外部dex主要流程如下:

  1. 从服务器下载插件apk到手机SDCard
  2. 读取插件apk中的dex,生成对应的DexClassLoader
  3. 使用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

  1. 把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);
        }

    }
  1. 在App启动的时候调用这个方法,在这个demo中,我重写了MainActivity的attachBaseContext方法,在里面做这个事情
@Override
    protected void attachBaseContext(Context newBase) {
        super.attachBaseContext(newBase);
        try {
            Utils.extractAssets(newBase, apkName);
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
  1. 加载插件 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());
  1. 调用生成的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.总结

上一篇 下一篇

猜你喜欢

热点阅读