再也不怕面试官问我类加载器了 超详细解析Android/Java之ClassLoader 双亲委派模型及热部署实践

ClassLoader

  • 前言
  • ClassLoader(类加载器)是什么
    • ClassLoader分类
    • 加载器初始化过程
    • ClassLoader传递性
    • Class加载方式
    • Class加载方法
    • Class相等原则
  • 双亲委派模型
    • AppClassLoader.loadClass
    • ClassLoader.loadClass
    • AppClassLoader.getAppClassLoader
    • 模型的作用
    • 线程上下文类加载器
  • 自定义ClassLoader--热部署
    • 加载本地文件系统Class
    • 类型强转异常
    • 破坏双亲委派模型
    • 加载网络Class数据
  • Android中的类加载器
    • BaseDexClassLoader
    • PathClassLoader DexClassLoader
    • DexPathList
    • BaseDexClassLoader.findClass
    • DexPathList.findClass

在这里插入图片描述

前言

ClassNotFoundExcetpion相信很多人碰到过,但是你有想过它是怎么出现的吗?它背后又牵扯到了多少不为人知的事情呢?在很长一段时间里真是被它折磨的死去活来,怎么办呢,没办法绕开它就只能解决它了,于是就有了这篇文章

我们平时开发中可能接触类加载器的情况不是很多,但是它在很多开源框架都有用到,比如在类层次划分,OSGI,热部署,代码加密,插件化,热更新等领域作用显著

像一些字节码加密技术其实就是依靠定制的ClassLoader实现的:先通过加密算法对字节码文件内容进行加密,运行时依靠定制的ClassLoader对class文件内容进行解密,最后再将解密后的字节码加载到内存中,这样就能一定程度上防止服务器上的一些核心class文件被攻击,导致不可估量的损失

废话不多说,接下来我就带大家剖析ClassLoader的一点一滴,让你不论在开发中还是面试中都能对ClassLoader应对自如

ClassLoader(类加载器)是什么

虚拟机设计团队把类加载过程中的通过一个类的全限定名来获取描述此类的二进制字节流这个动作放到JVM外部去实现,以便让应用程序自己决定如何去获取所需的类,实现这个动作的代码模块就是类加载器;简单点说类加载器就是用来加载Class文件的,它负责将Class的字节码数据转换成JVM运行时数据区(内存)的Class对象

其中字节码的来源可以是磁盘上的.class文件,jar包或者war包中的class,网络上的字节流,运行时计算生成(动态代理),数据库中读取等,字节码的本质是一个byte[],它有特定的复杂的内部格式

ClassLoader分类

从Java虚拟机的角度看,只有两种不同的类加载器,一种是启动类加载器,它是由C++实现的,是虚拟机的一部分;另一种就是所有其它的类加载器,它们都是由Java语言实现的,独立于虚拟机外部,全是ClassLoader的子类

但是从我们开发者的角度看,绝大部分的Java程序都会接触或使用到以下三种类加载器

  • BootstrapClassLoader:又称为启动类加载器,负责加载位于JAVA_HOME/lib目录下,或者-Xbootclasspath参数所指定的路径,同时得是虚拟机能够识别的类库;这里的识别是根据文件名识别,比如说典型的rt.jar(我们常用的一些 java.util.xxx,java.io.xxx,java.nio.xxx,java.lang.xxx 等都在里面),如果名称不符合,即使放在目录里也不会被加载;由于该加载器是由C++实现的,我们在Java中看不到它,也就无法被开发者直接拿到它的引用;如果我们需要在自定义的ClassLoader中将加载请求委托给它,只用将指向父加载器的引用置为null即可

  • ExtensionClassLoader:又称为扩展类加载器,由sun.misc.Launcher$ExtClassLoader实现,负责加载位于JAVA_HOME/lib/ext目录下(或者java.ext.dirs系统变量所指定的路径)的类库,比如swing系列,内置的js引擎,xml解析器等,开发者可以直接使用标准扩展类加载器

  • ApplicationClassLoader:又称为应用程序类加载器,由sun.misc.Launcher$AppClassLoader实现,负责将环境变量CLASSPATH目录下的类库加载到内存中;开发者可以直接通过Class.getSystemClassLoader得到,因为这个方法名所以一般也称它为系统类加载器;我们自己编写的类和使用的第三方jar都是由它加载的,前提是我们程序没有自定义ClassLoader,那它就是程序中默认的类加载器

当然除了这些加载器,还有其它的,比如: URLClassLoader(是ExtClassLoader和AppClassLoader的父类),它有个特点就是可以加载任意路径下的文件,它可以加载网络上的jar和class文件,可以从文件系统目录里加载;

加载器初始化过程

在虚拟机启动的时候会初始化BootstrapClassLoader,然后在Launcher类中加载ExtClassLoader和AppClassLoader,并将AppClassLoader的父加载器置为ExtClassLoader,同时ExtClassLoader的父加载器置为null(下面会说到)

ClassLoader传递性

在A类中引用另一个B类的时候(比如继承,实现,实例化),这个B类由哪个加载器去加载它呢?虚拟机的策略是使用A类的Class对象的CLassLoader来加载B类Class(每个Class对象内部都维护了一个ClassLoader属性来记录加载自己的类加载器)

Class加载方式

  • 隐式加载:不在代码里显示调用ClassLoader.loadClass加载类,而是通过JVM来自动加载需要的类到内存;比如某个类继承或者引用某个类,JVM发现这个类不在内存中,就会自动将这个类加载到内存
  • 显式加载:显示调用ClassLoader.loadClass方法或者Class.forName来加载类

Class加载方法

ClassLoader是一个抽象类,下面列举其中几个重要方法

  • ClassLoader getSystemClassLoader():获取一个系统类加载器,这是我们自定义加载器的默认委托父加载器,也是用于启动应用程序的类加载器,同时也是默认的当前线程的上下文类加载器

  • ClassLoader getParent():获取当前加载器的父加载器,所有的加载器有且只有一个父加载器,ExtClassLoader的父加载器是启动类加载器,但是它是C++编写的,所以返回null

  • Class loadClass(String name):参数name是一个类的全限定名,如"com.mango.school.Teacher",如果找不到将抛出ClassNotFoundException;这个方法最终调用内部一个重载的方法loadClass(String name, boolean resolve),其中第二个参数默认传false,告诉加载器不需要解析这个类

  • Class defineClass(String name, byte[] b, int off, int len):参数name同样是类的全限定名,参数byte[]是Class文件的字节数组,这个数组可以是本地文件获取,也可以通过网络获取

  • Class findSystemClass(String name):参数name是一个类的全限定名,它从本地文件系统载入Class文件,如果不存在将抛出ClassNotFoundException,该方法是JVM默认使用的装载机制

  • Class findLoadedClass(String name):判断一个Class文件是否已经被加载到JVM,如果没有加载就返回null

Class相等原则

如何判断JVM中两个Class对象是否相等呢?是来源于同一个Class文件吗?

比较两个Class对象是否相等,要同时满足来源于同一个Class文件,被同一个虚拟机加载,同时还要加载它们的类加载器也是同一个;这里的相等是通过Class对象的equals方法,isAssignableFrom方法,isInstance方法返回的结果,同时还包括instanceof关键字做对象所属关系判定等情况

双亲委派模型

不管是JVM还是Dalvik,类加载时都遵循双亲委派模型,那这到底是个什么东西呢?

简单点说就是当一个类加载器收到了类加载的请求,它不会立即自己干,而是将其委托给父加载器去完成;以此类推,这样所有的加载请求最终都会传到最顶层的启动类加载器,只有当父加载器无法完成类加载的请求,子加载器才会自己去加载

这里有三点需要注意:

  • 第一就是在该模型下,类加载器是分层次的,启动类加载器是最顶层的
  • 第二就是什么情况下父加载器完成不了子加载器委托过来的请求呢?那就是在它自己的搜索范围内找不到所需要加载的类,比如你让启动类加载器去加载一个自定义的类Student,那它在JAVA_HOME/lib目录下肯定是找不到的
  • 第三就是双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父加载器

其实还要注意的一点就是双亲委派模型在JDK1.2以后被引入,虽然是几乎应用于所有的Java程序中,但它不是一个强制性约束,只是Java推荐给开发者的一种类加载器实现方式,你在自定义的类加载器中完全可以重写loadClass方法抛弃该模型(后面将说到)

接下来我们从代码去了解双亲委派模型,要知道我们自己写的Java程序的类加载器默认是AppClassLoader,而且它加载类使用的方法基本上是loadClass方法,见下方

AppClassLoader.loadClass

public Class loadClass(String var1, boolean var2) throws ClassNotFoundException {
    int var3 = var1.lastIndexOf(46);
    if (var3 != -1) {
        SecurityManager var4 = System.getSecurityManager();
        if (var4 != null) {
            var4.checkPackageAccess(var1.substring(0, var3));
        }
    }

    if (this.ucp.knownToNotExist(var1)) {
        Class var5 = this.findLoadedClass(var1);
        if (var5 != null) {
            if (var2) {
                this.resolveClass(var5);
            }

            return var5;
        } else {
            throw new ClassNotFoundException(var1);
        }
    } else {
        return super.loadClass(var1, var2);
    }
}
  • 这里首先判断调用线程有没有权限访问指定的类,如果没有将抛出SecurityException;
  • 接下来通过native方法判断指定类是否已加载,如果加载过,那就通过findLoadedClass找到Class对象返回;如果找不到就抛出ClassNotFoundException异常
  • 如果这个类没有被加载,那就调用父类的loadClass方法去加载类

接下来从ExtClassLoader看看,当你看到它的源码时,你发现这个类没有重写父类的loadClass方法;而且从这两个类的层级可以看到它们都是ClassLoader的子类,且中间的几个父类都没有重写loadClass方法
再也不怕面试官问我类加载器了 超详细解析Android/Java之ClassLoader 双亲委派模型及热部署实践_第1张图片
再也不怕面试官问我类加载器了 超详细解析Android/Java之ClassLoader 双亲委派模型及热部署实践_第2张图片

那么最终的类加载逻辑就在ClassLoader里了

ClassLoader.loadClass

	private final ClassLoader parent;//用于委派的父类加载器的引用

    public Class loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }
    protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
  • 首先通过findLoadedClass方法分析该类是否已经加载过,如果加载过,直接返回
  • 如果没有加载,先判断委派的父类加载器是否为null,不为null就交给父加载器去加载;为null就通过findBootstrapClassOrNull方法交给启动类加载器加载(最终会调用到native层)
  • 如果父加载器或者启动类加载器无法完成加载,将会抛出异常,结果就是c=null,那就通过findClass方法自己去加载;但是该方法没有具体实现,Java推荐开发者自定义的类加载器去重写这个方法,不要重写loadClass方法,以遵循双亲委派模型

双亲委派的逻辑就是这个方法里体现的,不知道说到这里大家对双亲委派模型有没有点了解呢?从代码里能看到不管是AppClassLoader还是ExtClassLoader,接到加载任务时第一时间都是将任务委派给父加载器做,也就是这个ClassLoader类的parent变量,而且这个变量只有通过构造方法赋值

private ClassLoader(Void unused, ClassLoader parent)
protected ClassLoader(ClassLoader parent)

那要想找到这个parent到底指的是谁就去看看AppClassLoader和ExtClassLoader的实例化过程

AppClassLoader.getAppClassLoader

static class AppClassLoader extends URLClassLoader {
       
     final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);

     public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
         final String var1 = System.getProperty("java.class.path");
         final File[] var2 = var1 == null?new File[0]:Launcher.getClassPath(var1);
         return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction() {
             public Launcher.AppClassLoader run() {
                 URL[] var1x = var1 == null?new URL[0]:Launcher.pathToURLs(var2);
                 return new Launcher.AppClassLoader(var1x, var0);
             }
         });
     }

     AppClassLoader(URL[] var1, ClassLoader var2) {
         super(var1, var2, Launcher.factory);
         this.ucp.initLookupCache(this);
     }
}

很明显它是通过一个静态方法接收一个ClassLoader,然后调用了父类的构造方法,并且将其传递过去;到这里还是看不到这个ClassLoader的真身是谁,继续看它被谁调用

public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }

        try {
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }
}
  • AppClassLoader是Launcher的内部类,从这里可以看到实例化AppClassLoader的时候传递的ClassLoader参数是ExtClassLoader

  • 通过对ExtClassLoader的实例化分析可以知道最终这个parent的变量是null;同时对ClassLoader.loadClass方法可以知道,如果parent是null,将会把任务委托给启动类加载器

最终分析后可以得出双亲委派模型图如下(要注意这里的类加载器的父子关系不是以继承的关系实现的,而是通过组合的方式,即内部变量parent指向父加载器)

再也不怕面试官问我类加载器了 超详细解析Android/Java之ClassLoader 双亲委派模型及热部署实践_第3张图片

模型的作用

在上面说过,在同一个JVM中,对于任意一个Class,需要由加载它的类加载器和这个Class本身一同确定其在JVM中的唯一性,那么在双亲委派模型中,类就会随着加载它的ClassLoader一起具备了一种带有优先级的层级关系,通过这种层级关系可以避免类的重复加载,同时也能保证Java核心类库的安全性

为什么这么说呢?比如类java.lang.Object,它存放在rt.jar中,是由启动类加载器加载的,那么在双亲委派模型下任何加载器收到对java.lang.Object的加载请求,最终都会将其委托给模型最顶层的启动类加载器加载,这样就能保证java.lang.Object在任何加载器中都是同一个,包括其它的比如String等所有的Java核心类库都是由启动类加载器加载,保证了Java系统的安全性和稳定性;相反,如果没有这种模型,由各个加载器自行加载,如果用户自己定义一个java.lang.Object的Java文件,并放在程序的ClassPath中,使用自定义的加载器加载,那系统中将出现多个Object的Class,显然这种情况下Java体系中最基础的行为都将不能正常运行

线程上下文类加载器

Context Thread Loader,是从jdk1.2引入的,通过Thread.getContextClassLoader()和Thread.setContextClassLoader(ClassLoader cl)来获取和设置当前线程的上下文类加载器;如果没有设置,那么当前线程将继承父线程的上下文类加载器,什么叫父线程呢?就是创建了当前线程的线程;程序启动时的main线程的Context Thread Loader是AppClassLoader,这也就是意味着如果没有手动设置,那么你的程序的所有线程的Context Thread Loader都是AppClassLoader;

在线程运行的时候可以通过Thread.getContextClassLoader()获取当前线程的上下文类加载器,代码如下

   ClassLoader loader = Thread.currentThread().getContextClassLoader();
   System.out.print(loader.toString()+"\n");
   System.out.print(loader.getParent().toString()+"\n");
   System.out.print(loader.getParent().getParent());

打印结果

sun.misc.Launcher$AppClassLoader@4aa298b7
sun.misc.Launcher$ExtClassLoader@63961c42
null

可以看到当前线程上下文类加载器是AppClassLoader,其父加载器是ExtClassLoader,而ExtClassLoader的父加载器是null(在Java层无法获得它的句柄)

有没有感觉到疑问呢?就是这个上下文类加载器有什么用呢?

要知道双亲委派模型是自下而上的加载流程,这种模型有优点自然就有缺点;假设现在只有双亲委派模型,而Java有很多SPI(Service Provider Interface 即服务提供接口),允许第三方为这些接口提供实现,比如说 JDBC,JCE,JAXP,JNDI 等等;这些接口是Java核心类库提供,而它们的实现类是第三方提供的,通常作为Java应用依赖的Jar包

所以带来的问题就是属于核心类的SPI由启动类加载器加载,但是这些接口要用到这些实现类,那在实现类里就会使用调用方的类加载器进行加载,可是它的实现类却不能被启动类加载器识别,也就谈不上加载了,同时由于该模型的存在,它又不能将其交给子类系统类加载器加载;那么这时候双亲委派模型就存在问题了!

这是一个反向的过程,即父加载器需要请求子加载器来加载类,那么线程上下文类加载器提供了一个新的体系,用于打破这个规则,可以在执行过程中抛弃双亲委派模型,而是用线程上下文类加载器来加载需要的类,这样就可以显示的指定类加载器。大部分的 java应用如 jboss,tomcat 都是采用 contextClassLoader 来处理 web 服务,还有一些采用 hot swap 的框架,也是采用线程上下文类加载器


自定义ClassLoader–热部署

实现自己的ClassLoader只需要继承ClassLoader即可,尽量不要重写loadClass方法,而是重写findClass方法,即只需要关心从哪里获取Class数据就行了,最后将数据传给defineClass方法进行加载;同时还需要指定自己的父加载器,通常在构造方法里传入,如果不指定,那默认的父加载器是AppClassLoader(前提是调用父类无参的构造方法)

这里有两个问题:

  • 有的人可能疑问了,为啥不继承AppClassLoader呢?因为它和ExtClassLoader都是Launcher的静态内部类,都是包访问权限的,你没法继承

  • 如果我强行将自己的parent置为null,那么会带来什么后果?这种情况下,你的父加载器就是启动类加载器,那么可以加载到/lib下的类,但此时就不能够加载/lib/ext目录下的类了

所以自定义加载器,要注意几点:

  • 尽量不要重写loadClass方法,遵循双亲委派模型,保证正确加载lib或者ext目录下的类库
  • 正确设置父加载器 ,保证正确加载lib或者ext目录下的类库
  • 继承ClassLoader,重写findClass方法,正确加载字节码数据

加载本地文件系统Class

这里以热部署为例吧,在不重启服务器的情况下可以修改类的代码并使其生效

/**
 * Author: Mangoer
 * Time: 2019/3/20 22:37
 * Version:
 * Desc: TODO(自定义类加载器)
 */
public class LocalFileClassLoader extends ClassLoader {


    //class文件路径
    private String classPath;
    //类的全限定名
    private Set ownClass;

    public LocalFileClassLoader (String classPath, Set ownClass) {
        super();
        this.classPath = classPath;
        this.ownClass = ownClass;
    }
    
    public void setClass(String name){
        ownClass.add(name);
    }
    

    @Override
    public Class loadClass(String name) throws ClassNotFoundException {
        //判断这个类有没有被加载
        Class clazz = findLoadedClass(name);
        //如果没有被加载且是我们自定义的类,那就自己加载
        if (clazz == null && ownClass.contains(name)) {
            clazz = findClass(name);
            if (clazz != null) {
                return clazz;
            }
        }
        return super.loadClass(name);
    }

    @Override
    protected Class findClass(String name) throws ClassNotFoundException {
        //获取class文件字节数组
        byte[] data = new byte[0];
        try {
            data = getClassByte(name);
        } catch (Exception e) {
            e.printStackTrace();
        }
        if (data == null) {
            throw new NullPointerException("this class not found");
        }
        //将二进制数据转换成class对象
        return defineClass(name,data,0,data.length);
    }

    private byte[] getClassByte(String name) throws Exception {
        
        byte[] data = null;
        String className = name.substring(name.lastIndexOf(".") + 1) + ".class";
        File file = new File(classPath+className);
        if (!file.exists()) return data;

        FileInputStream fis = new FileInputStream(file);
        byte[] buff = new byte[1024 * 2];
        int len ;
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        while ((len = fis.read(buff)) != -1) {
            bos.write(buff,0,len);
        }
        data = bos.toByteArray();
        fis.close();
        bos.close();
        return data;
    }


}

代码比较简单,就不多说了

接下来定义一个功能类,模仿用户登录

public class LoadTest {

    public void loadUser(){
        System.out.print("新用户老赵登录了" + "\n");
    }

}

接下来将这个LoadTest进行编译,放到某个目录,比如我这里是 E:\class\LoadTest.class;这里如果使用javac命令编译java文件,记得加上-encoding UTF-8,因为Windows操作系统默认机内码是GBK,而javac命令采用的是utf-8格式,不加的话会报错

最后写测试类

public class Test {

    public static void main(String[] args){

        while (true) {

            String classPath = "E:\\class\\";
            String className = "com.mango.compontent.LoadTest";

            Set set = new HashSet<>();
            set.add(className);
            LocalFileClassLoader loader = new LocalFileClassLoader (classPath,set);

            try {
                Class clazz = loader.loadClass(className);
                Object o = clazz.newInstance();
                clazz .getMethod("loadUser").invoke(o);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }

            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

逻辑就是每三秒执行一次loadUser方法

看看打印结果:
再也不怕面试官问我类加载器了 超详细解析Android/Java之ClassLoader 双亲委派模型及热部署实践_第4张图片

现在程序不停,我将打印内容换下,然后将编译后的文件去替换掉原来的class文件,看看什么结果

public class LoadTest {

    public void loadUser(){
        System.out.print("新用户小米登录了" + "\n");
    }

}

再也不怕面试官问我类加载器了 超详细解析Android/Java之ClassLoader 双亲委派模型及热部署实践_第5张图片

可以看到我并没有重启这个程序,只是换了一个class文件,但是方法已经修改了;这只是热部署的一个简单应用,实际情况会复杂的多,比如OSGI的核心理念就是应用模块(bundle)化,对于每一个bundle,都有自己的类加载器,当需要更新bundle时,把bundle和它的类加载器一起替换掉,就可以实现模块的热替换

类型强转异常

不知道你们有没有注意main方法中,当通过clazz.newInstance()获取到Object后,并不是将其进行强转成LoadTest再执行其方法,而是通过反射去执行方法,为什么这样做呢?

因为在强转成LoadTest的过程中,其得到的新的class对象是由当前线程的默认加载器进行加载得到LoadTest对象,但是这个默认加载器是AppClassLoader,原因在上面说过,那么你就是把通过自定义的加载器加载的Class对象转换成AppClassLoader加载的Class对象;上面说过,一个类就算全限定名一样,但是加载它们的加载器不同,那么这两个Class对象也是两个不同的对象,那么进行强制转换肯定会抛出ClassCastException;所以就需要通过反射进行操作

破坏双亲委派模型

上面在提到自定义ClassLoader的时候讲到为了遵循双亲委派模型,不要重写loadClass方法,但是我这里不仅重写了,还改变了其加载逻辑,使用我们自己的加载器加载,这其实是破坏了双亲委派模型

在Java中,破坏双亲委派模型的情况是存在的,Java自己就破坏了双亲委派模型,比如JNDI使用线程上下文加载器加载所需要的SPI实现代码,其实是让父加载器请求子加载器去完成类的加载;还有JDBC加载数据库驱动的过程中在启动类加载器的类中通过线程上下文加载器加载了应用程序的实现类;同时OSGI实现模块化热部署的过程也是破坏双亲委派模型;这些具体原理读者有兴趣可以自行Google,这里就不多叙述了

加载网络Class数据

这里场景就是 .class文件存放在服务器,当客户端出现了一个bug,需要替换掉某个Class文件,为了避免重新更换客户端版本,就可以通过网络将class数据下载到本地;或者需要紧急打补丁,直接将获取到的字节数据加载到内存

类加载器如下

public class NetworkClassLoader extends ClassLoader {

    //Class文件的网络地址
    private String classUrl;

    public NetworkClassLoader(String classUrl) {
        super();
        this.classUrl = classUrl;
    }

    @Override
    protected Class findClass(String name) throws ClassNotFoundException {

        byte[] data = getClassData();
        if (data == null) {
            throw new ClassNotFoundException("this class ["+name+"] not found");
        }
        return defineClass(name,data,0,data.length);
    }

    /**
     * 这里还可以将网络获取的字节数组保存在本地,替换掉原来的class文件
     * 也可以不替换,直接加载这个字节数组
     * @return
     */
    private byte[] getClassData() {

        byte[] data = null;

        try {
            URL url = new URL(classUrl);
            InputStream stream = url.openStream();
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            byte[] buff = new byte[1024 * 2];
            int len;
            while ((len = stream.read(buff)) != -1) {
                bos.write(buff,0,len);
            }
            data = bos.toByteArray();
            stream.close();
            bos.close();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

        return data;

    }
}

加载

    public static void main(String[] args){

        String url = "http://localhost:8080/com/mango/compontent/LoadTest.class";

        NetworkClassLoader classLoader = new NetworkClassLoader(url);
        try {
            Class clazz = classLoader.loadClass("com.mango.compontent.LoadTest");
            Object o = clazz.newInstance();
            clazz.getMethod("loadUser").invoke(o);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }


    }

其实跟LocalFileClassLoader的实现差不多


Android中的类加载器

Android源码在线查阅

Android中的类加载器和Java类加载器基本上差不多,同时也是遵循双亲委派模型的,Android中常用的类加载器如图所示:

再也不怕面试官问我类加载器了 超详细解析Android/Java之ClassLoader 双亲委派模型及热部署实践_第6张图片

其中我们开发者接触最多的就是PathClassLoader和DexClassLoader

  • BootClassLoader:有点类似于Java中的BootStrap ClassLoader,它是用于加载Android Framework层class文件;和BootStrap ClassLoader不同的是BootClassLoader是ClassLoader内部类,由java代码实现而不是c++实现,是Android平台上所有ClassLoader的最终parent,这个内部类是包内可见,所以我们没法使用

  • BaseDexClassLoader:PathClassLoader和DexClassLoader的父类,它们两的主要逻辑都是在这个类里实现的

  • PathClassLoader:有点类似于Java的AppClassLoader,这个加载器的作用只有一个,就是只能用于加载系统类和已经安装到系统中的apk里的dex文件(/data/app目录),是Android默认使用的类加载器,可以在Activity里通过this.getClass().getClassLoader().toString()获知

  • DexClassLoader:这个就类似于在Java中我们自定义的加载器,它可以用于加载指定目录中的dex文件,比PathClassLoader更灵活,所以一般在做插件化或者热更新的时候要使用到这个加载器

我们可以通过ClassLoader的loadClass方法看看是不是遵循双亲委派模型的

    protected Class loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            Class c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                }
            }
            return c;
    }

可以看到跟上面分析的是一样的,这里就不多叙述了,值得一说的是当父加载器为null的时候,会调用findBootstrapClassOrNull方法,但是这个方法是空的实现,返回null,到下面还是调用findClass方法去加载;也就是说如果你在自定义的类加载器中将父加载器置为null,那么只能自己去加载了

BaseDexClassLoader

public class BaseDexClassLoader extends ClassLoader {

    private final DexPathList pathList;

    /**
     * @param 包含class和resource的jar/apk文件路径(包括SD卡),如果有多个路径,使用特殊符号File.pathSeparator分割(默认是 : )
     * @param odex文件路径,必须是手机内部存储路径;其中dex文件是在apk中,apk进行安装,安装程序将其解压到这个目录,解压其实是一个优化的过程,将dex文件优化成odex文件;这个参数可以传null,默认是/data/dalvik-cache
     * @param so文件路径,可以是多个,使用File.pathSeparator分割,可以传null
     * @param 父加载器
     */
    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
           String librarySearchPath, ClassLoader parent) {
       super(parent);
       // 实例化一个DexPathList对象,这个类的具体操作都是交给DexPathList实现的
       this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
    }

	......
}

PathClassLoader DexClassLoader

public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }

    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}
public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
       super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
    }
}

这两个类的代码很简单,只提供了几个构造方法,然后就调用父类的构造方法,并将参数传递过去;关键就是它们提供的构造方法决定了这两个类的功能

  • 首先看PathClassLoader 构造方法,它缺少了一个很重要的参数optimizedDirectory,也就是dex文件优化后的路径,Dalvik从这个路径去加载文件;但是PathClassLoader不给开发者提供这个参数,那它的默认值就是/data/dalvik-cache,这就决定了PathClassLoader被设定成只能加载Android系统应用dex文件和已安装的android应用dex文件

  • 再看 DexClassLoader 的构造方法,它四个参数都可以设置,也就是说能设置虚拟机加载类的路径,因为这个灵活性,开发者就可以使用它去加载我们自己的dex文件,因此我们一般会采用DexClassLoader做动态加载

总的来说DexClassLoader可用于加载指定路径下的APK、DEX和JAR文件,也可以从SD卡进行加载,能支持插件化加载、热修复等;而PathClassLoader只可用于加载已经安装到系统中的apk中的.dex文件,不支持动态加载等业务

这两个加载器并没有实际作用,只是BaseDexClassLoader提供给开发者的一个代理,而BaseDexClassLoader的操作又委托给了DexPathList去实现,你可能觉得没接触过它,但是你是不是碰到过下面类似的错误

java.lang.ClassNotFoundException: Didn't find class "com.xxx.xxx.xxx.xxx" on path: 
DexPathList[[zip file "/data/app/xxx.apk"],nativeLibraryDirectories=[/data/app-lib/xxx, /system/lib]]

这里就有这么一个词DexPathList,说的就是它,看看它的toString方法

    @Override 
    public String toString() {
        List allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
        allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);

        File[] nativeLibraryDirectoriesArray =
                allNativeLibraryDirectories.toArray(
                    new File[allNativeLibraryDirectories.size()]);

        return "DexPathList[" + Arrays.toString(dexElements) +
            ",nativeLibraryDirectories=" + Arrays.toString(nativeLibraryDirectoriesArray) + "]";
    }

是不是感觉有点意思,那么我们看下它的源码

DexPathList

final class DexPathList {
    // 声明dex文件后缀名
    private static final String DEX_SUFFIX = ".dex";
    //分隔符
    private static final String zipSeparator = "!/";

    /** 传入的父加载器 */
    private final ClassLoader definingContext;

    /**
     * Element内部封装了 dex/resource/native library 路径
     * 这个是用来存放dex文件路径的数组
     */
    private Element[] dexElements;

    /** 这个是用来存放所有动态库路径的数组 */
    private final Element[] nativeLibraryPathElements;

    /** app动态库文件列表. */
    private final List nativeLibraryDirectories;

    /** 系统动态库文件列表. */
    private final List systemNativeLibraryDirectories;

    /** 各种IO异常数组. */
    private IOException[] dexElementsSuppressedExceptions;


    public DexPathList(ClassLoader definingContext, String dexPath,String librarySearchPath, File optimizedDirectory) {

        //如果父加载器为null,那么抛出异常
        if (definingContext == null) {
            throw new NullPointerException("definingContext == null");
        }
        //如果dex文件路径为null 抛出异常,源文件都不存在,那还加载个卵子
        if (dexPath == null) {
            throw new NullPointerException("dexPath == null");
        }
        //如果将dex文件优化后放置的文件目录不为空
        if (optimizedDirectory != null) {
            if (!optimizedDirectory.exists())  {//且这个目录不存在,那就抛出异常,你让我加载这个目录的文件,结果文件不存在,你逗我玩呢
                throw new IllegalArgumentException(
                        "optimizedDirectory doesn't exist: "
                        + optimizedDirectory);
            }

            //如果这个要加载的目标目录不能读写,抛出异常,同样还是在逗我玩
            if (!(optimizedDirectory.canRead()
                            && optimizedDirectory.canWrite())) {
                throw new IllegalArgumentException(
                        "optimizedDirectory not readable/writable: "
                        + optimizedDirectory);
            }
        }

        this.definingContext = definingContext;

        ArrayList suppressedExceptions = new ArrayList();
        // 通过makeDexElements方法将dex文件路径保存到数组
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,suppressedExceptions, definingContext);

        //通过splitPaths方法保存应用动态库路径到list
        this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
        //通过splitPaths方法保存系统动态库到list
        this.systemNativeLibraryDirectories = splitPaths(System.getProperty("java.library.path"), true);
        List allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
        //合并两个数组
        allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
        //将所有的动态库路径保存到数组
        this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories,
                                                          suppressedExceptions,
                                                          definingContext);

        if (suppressedExceptions.size() > 0) {
            this.dexElementsSuppressedExceptions =
                suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
        } else {
            dexElementsSuppressedExceptions = null;
        }
    }
    
    ......
}

这里有一个很重要的属性:dexElements,它是用来存放最终的dex文件的数组

接下来看看几个重要方法,第一个就是makeDexElements,它第一个参数是通过splitDexPath(dexPath)方法返回的,前面在说到构造方法的时候讲到可以传多个dex文件路径,用File.pathSeparator分割就行了,所以这个方法就是将dexPath分割装到List里去返回

DexPathList.makeDexElements

    private static Element[] makeDexElements(List files, File optimizedDirectory,
                                             List suppressedExceptions,
                                             ClassLoader loader) {
        return makeElements(files, optimizedDirectory, suppressedExceptions, false, loader);
    }

DexPathList.makeElements

    /**
    *  files:待加载的类的apk,jar,dex的路径
    *  optimizedDirectory:优化后的文件目录
    *  其它参数没啥可说的
    */
    private static Element[] makeElements(List files, File optimizedDirectory,
                                          List suppressedExceptions,boolean ignoreDexFiles,ClassLoader loader) {
        //Element你就理解为一个保存dex/resource/native library 目录的对象
        Element[] elements = new Element[files.size()];
        int elementsPos = 0;
        /*
         * 遍历dex文件数组,打开所有文件并预先加载(直接或包含)dex文件
         */
        for (File file : files) {
            File zip = null;
            File dir = new File("");
            DexFile dex = null;
            String path = file.getPath();
            String name = file.getName();

            if (path.contains(zipSeparator)) {
                String split[] = path.split(zipSeparator, 2);
                zip = new File(split[0]);
                dir = new File(split[1]);
            } else if (file.isDirectory()) {
                // We support directories for looking up resources and native libraries.
                // Looking up resources in directories is useful for running libcore tests.
                elements[elementsPos++] = new Element(file, true, null, null);
            } else if (file.isFile()) {
                if (!ignoreDexFiles && name.endsWith(DEX_SUFFIX)) {
                    // Raw dex file (not inside a zip/jar).
                    try {
                        dex = loadDexFile(file, optimizedDirectory, loader, elements);
                    } catch (IOException suppressed) {
                        suppressedExceptions.add(suppressed);
                    }
                } else {
                    zip = file;

                    if (!ignoreDexFiles) {
                        try {
                            dex = loadDexFile(file, optimizedDirectory, loader, elements);
                        } catch (IOException suppressed) {
                            suppressedExceptions.add(suppressed);
                        }
                    }
                }
            } else {
                System.logW("ClassLoader referenced unknown path: " + file);
            }

            if ((zip != null) || (dex != null)) {
                elements[elementsPos++] = new Element(dir, false, zip, dex);
            }
        }
        if (elementsPos != elements.length) {
            elements = Arrays.copyOf(elements, elementsPos);
        }
        return elements;
    }

这个方法主要就是遍历传进来的文件数组,然后进行判断,如果是目录,直接将这个file作为参数构造Element对象并添加到elements数组;如果是其它文件就通过loadDexFile方法进行加载,然后将加载后的dex文件添加到elements数组;看下这个方法

DexPathList.loadDexFile

private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader,Element[] elements)
      if (optimizedDirectory == null) {
          return new DexFile(file, loader, elements);
       } else { 
           String optimizedPath = optimizedPathFor(file, optimizedDirectory);
           return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);
       } 
 } 

这里如果optimizedDirectory 为null,也就是dex文件优化后存放的路径是空,那就直接通过构造方法构造一个DexFile返回;
如果不为null,那就先调用optimizedPathFor方法:第一,如果不是.dex结尾的文件,先改成.dex文件后缀,然后用optimizedDirectory和新的文件名构造一个File并返回该File的路径;第二,是.dex后缀的文件就直接用optimizedDirectory和文件名构造File并返回路径;最后通过DexFile.loadDex方法构造一个DexFile文件,其中第二个参数optimizedPath就是我们要加载的文件优化后存放的路径

dex文件处理完了,接下来构造方法就是处理 .so文件了,操作是类似的,这里就不多叙述了;到这里我们已经将我们的class文件和so文件添加进dexElements数组和nativeLibraryPathElements中了


BaseDexClassLoader.findClass

那就要想了,把这么多dex文件都添加到这个数组的作用是什么呢?这就要从加载的时候开始看了

当我们使用DexClassLoader加载自己的文件的时候,是使用它的loadClass方法,要知道loadClass的真正实现是在ClassLoader类中,任何子类并没有改变这个逻辑,这个方法在上面有讲到,如果父加载器都没有加载出我们指定的文件,那么就交给findClass方法去加载,但是这个方法ClassLoader并没有具体实现,交给子类去做,而DexClassLoader的父类是BaseDexClassLoader,那就去它里面看看

    @Override
    protected Class findClass(String name) throws ClassNotFoundException {
        List suppressedExceptions = new ArrayList();
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
     }

这里面并没有自己的操作,而是交给DexPathList去做了,注意看这里如果没有加载成功,会抛出一个ClassNotFoundException异常,这个异常的具体信息是不是跟你平常碰到的基本一样呢?那它什么情况下会抛出这个异常呢?去里面看看

DexPathList.findClass

public Class findClass(String name) { 
    for (Element element : dexElements) { 
        DexFile dex = element.dexFile;
        if (dex != null) { 
            Class clazz = dex.loadClassBinaryName(name, definingContext); 
          if (clazz != null) { 
              return clazz; 
          } 
        } 
    } 
    if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
}

可以看到加载一个类原来就是去遍历dexElements数组,将DexFile 和类名传到loadClassBinaryName方法去加载一个class对象出来;当然了如果加载不成功,就到上一步,就会抛出ClassNotFoundException异常

看到这里不知道你有没有点感悟,就是如果我把我自己想加载的类且不是本应用的类想办法添加到这个dexElements数组中,那是不是也可以加载到呢?当然了如果想成功加载还需要掌握更多的知识,这里只是一个开头

你可能感兴趣的:(【Framework源码解析】)