美文网首页android面试录Android开发经验谈Android开发
Android 简单热修复(上)——Java类加载器

Android 简单热修复(上)——Java类加载器

作者: nick_young | 来源:发表于2018-01-05 19:59 被阅读38次

    作为阳历新年的第一篇文章,本想把之前总结的用到实践中,简单写了个钟表,写着写着感觉索然无味(/ □ )。写完后,百无聊赖之际,随便翻看了些技术文章。让我眼前为之一亮的有两个:

    • Android 破解跳一跳
    • Android 简单热修复原理

    作为Android狗的我果断选择了热修复的介绍,在看完Android类加载器的源码后,对于简单的热修复原理算是了解了一些。遂作此文,以谨记。



    在介绍Android热修复原理之前,有必要了解下关于Java的类加载器的相关知识。在《深入理解Java虚拟机》一书中关于类加载的可以分为五个过程:

    1. 加载
      在加载过程中需要完成3件事情:
      1.1 通过一个类的全限定名来获取定义此类的二进制字节流。
      1.2 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构。
      1.3 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
    2. 验证
      这一阶段的主要目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
    3. 准备
      准备阶段是正式为类变量分配内存并设置类变量初始值的阶段 ,这些变量所使用的内存都将在方法区中进行分配。
    4. 解析
      解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
    5. 初始化
      初始化阶段是执行类构造器<clinit>()方法的过程。

    关于详细介绍,还是乖乖看书吧。
    OK,知道了类加载的过程,但是究竟是什么“东西”加载类呢?答案是类加载器(ClassLoader),也是今天的主题。
    简单说下类加载器的分类:

    • 启动类加载器(BootStrap ClassLoader):启动类加载器负责将<JAVA_HOME>\lib目录下中的,或者被-Xbootclasspath参数所指定的路径中的,并且被虚拟机识别的类库加载到虚拟机内存中(有点拗口)。通过System.getProperty("sun.boot.class.path")可知默认情况加载如下类库:
    C:\Program Files\Java\jdk1.8.0_131\jre\lib\resources.jar
    C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar
    C:\Program Files\Java\jdk1.8.0_131\jre\lib\sunrsasign.jar
    C:\Program Files\Java\jdk1.8.0_131\jre\lib\jsse.jar
    C:\Program Files\Java\jdk1.8.0_131\jre\lib\jce.jar
    C:\Program Files\Java\jdk1.8.0_131\jre\lib\charsets.jar
    C:\Program Files\Java\jdk1.8.0_131\jre\lib\jfr.jar
    C:\Program Files\Java\jdk1.8.0_131\jre\classes
    
    • 扩展类加载器(Extension ClassLoader):扩展类加载器用于将<JAVA_HOME>\lib\ext中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。扩展类加载器加载的类库(默认情况),可以看到其就是<JAVA_HOME>\lib\ext中的类库:


      扩展类加载器加载的类库
    • 应用程序类加载器(Application ClassLoader):用于加载用户类路径上所指定的类库,如果程序没有自定义过自己的类加载器,一般情况下这个就是这个程序的默认类加载器。应用程序类加载器加载的类库(默认情况),可以看到其加载的类库包括了<JAVA_HOME>\lib和<JAVA_HOME>\lib\ext目录下的类库,也就是说如果前两个没有找到要加载的类,也可以通过AppClassLoader去加载:


      应用程序类加载器加载的类库

    启动类加载器

    上面已经说过启动类加载器会加载的类库,下午我和一个大佬讨论了下关于java类是否按需加载。答案是:java类是按需加载,只有当需要用到这个类的时候才会加载这个类。在运行时添加-verbose:class参数,我们先看到被加载到内存中的类:

    启动时加载的类
    启动类加载了rt.jar中的类,我们可以通过反向来证明某个类是由启动类加载器加载:
    System.out.println(String.class.getClassLoader());
    

    在上面我们只是输出了一下String这个类的类加载器,结果如下:

    String类加载器
    我们可以知道其类加载器是null,这又是为什么呢?我们看下getClassLoader()这个方法的注释:
    注释
    从注释中我们可以知道如果返回值为null,那么代表此时的类加载器是BootStrap ClassLoader,所以上面所讲述的完全没毛病。

    扩展类加载器

    先看下默认的<JAVA_HOME>\lib\ext路径下的类库有什么:


    ext类库

    默认的路径下加载的类库并不是特别多,我们挑选其中的一个来测试下:

    System.out.println(JarFileSystemProvider.class.getClassLoader());
    
    测试扩展类加载器
    从结果中我们可以知道加载扩展类的加载器是sun.misc.Launcher类的内部类ExtClassLoader

    应用程序类加载器

    应用程序加载器用于加载当前程序的类库(默认情况下),按照上面的测试我们同样测试下:

    // UserModel为当前程序里的一个类
    System.out.println(UserModel.class.getClassLoader());
    

    运行结果:

    应用程序类加载器
    从结果中我们可以知道加载扩展类的加载器是sun.misc.Launcher类的内部类AppClassLoader
    类关系图:
    类关系图

    讲下每个类加载器的父亲:

    • BootStrap ClassLoader:无父类加载器
    • ExtClassLoader:父类加载器BootStrap ClassLoader
    • AppClassLoader:父类加载器ExtClassLoader

    关于三个类加载器的创建

    BootStrap ClassLoader

    Bootstrap ClassLoader是由C/C++编写的,它本身是虚拟机的一部分,所以它并不是一个JAVA类,也就是无法在Java代码中获取它的引用。

    ExtClassLoader的创建

    话不多少,还是先看下代码吧:

    Launcher.java:
    public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
            // 获得ExtClassLoader
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }
    
        try {
            // 将ExtClassLoader作为参数传入AppClassLoader中
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }
    
        Thread.currentThread().setContextClassLoader(this.loader);
        ......
    
    }
    
    static class ExtClassLoader extends URLClassLoader {
        public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
            // 获取了Ext的目录
            final File[] var0 = getExtDirs();
    
            try {
                return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() {
                    public Launcher.ExtClassLoader run() throws IOException {
                        int var1 = var0.length;
    
                        for(int var2 = 0; var2 < var1; ++var2) {
                            MetaIndex.registerDirectory(var0[var2]);
                        }
                        // 创建一个新的ExtClassLoader,传入文件数组
                        return new Launcher.ExtClassLoader(var0);
                    }
                });
            } catch (PrivilegedActionException var2) {
                throw (IOException)var2.getException();
            }
        }
    
        void addExtURL(URL var1) {
            super.addURL(var1);
        }
    
        public ExtClassLoader(File[] var1) throws IOException {
            // 父类构造方法,其中第二个参数为parent也就是当前ClassLoader的父类加载器
            // 这里传入的是null,也就是其父类加载器是BootStrap ClassLoader
            super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
            SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
        }
    
        private static File[] getExtDirs() {
            String var0 = System.getProperty("java.ext.dirs");
            File[] var1;
            if(var0 != null) {
                StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);
                int var3 = var2.countTokens();
                var1 = new File[var3];
    
                for(int var4 = 0; var4 < var3; ++var4) {
                    var1[var4] = new File(var2.nextToken());
                }
            } else {
                var1 = new File[0];
            }
    
            return var1;
        }
    
        ......
    }
    

    从代码中我们可以知晓:

    • ExtClassLoader是在Launcher中创建,并且指定其父类加载器为null(BootStrap ClassLoader)
    • 通过getExtDirs获得扩展类的目录文件数组

    我们看下getExtDirs输出:

    C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext
    C:\Windows\Sun\Java\lib\ext
    

    这个输出一个代表了<JAVA_HOME>\lib\ext路径,另一个则是默认的扩展类路径。

    AppClassLoader的创建

    Launcher的部分代码中可以知道ExtClassLoader作为参数传入AppClassLoader中,这里看下AppClassLoader类:

    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<Launcher.AppClassLoader>() {
                public Launcher.AppClassLoader run() {
                    URL[] var1x = var1 == null?new URL[0]:Launcher.pathToURLs(var2);
                    // 这里将传入的ExtClassLoader作为构造参数,说明其父类加载器为ExtClassLoader
                    return new Launcher.AppClassLoader(var1x, var0);
                }
            });
        }
    
        AppClassLoader(URL[] var1, ClassLoader var2) {
            super(var1, var2, Launcher.factory);
            this.ucp.initLookupCache(this);
        }
    
        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 {
                // 调用ClassLoader的loadClass
                return super.loadClass(var1, var2);
            }
        }
    
        ......
    }
    

    AppClassLoaderExtClassLoader作为父类加载器,并且重写了loadClass方法,用于校验。不过我在debug时发现System.getSecurityManager()返回值为null,所以推测这里需要自己实现安全管理。

    验证:

    ClassLoader classLoader = Main.class.getClassLoader();
    while (classLoader.getParent() != null) {
        System.out.println(classLoader);
        classLoader = classLoader.getParent();
    }
    System.out.println(classLoader);
    

    输出:

    sun.misc.Launcher$AppClassLoader@18b4aac2
    sun.misc.Launcher$ExtClassLoader@1540e19d
    

    类加载器的双亲委派机制

    双亲委派机制模型

    双亲委派机制:某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
    好处:使用双亲委派模型的好处在于Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存在在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的Bootstrap ClassLoader进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,那系统中将会出现多个不同的Object类,程序将混乱。因此,如果开发者尝试编写一个与rt.jar类库中重名的Java类,可以正常编译,但是永远无法被加载运行。
    (《深入理解Java虚拟机》)

    双亲委派机制的实现

    废话不多说,先上代码为敬:

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            // 第一步检查此类是否已经被加载,native层实现
            Class<?> c = findLoadedClass(name);
            // 如果没有被加载
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // 获取其父类加载器,并且调用loadClass()方法
                    // 如果父类加载器是BootStrap ClassLoader,则调用findBootstrapClassOrNull
                    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;
        }
    }
    

    这里看下loadClass的过程:

    1. 查看类是否已经被加载过,通过native方法实现。如果已经加载过,直接返回此Class
    2. 类没有被加载过,如果其父类加载器存在,调用父类加载器的loadClass方法加载Class
    3. 父类加载器不存在,调用findBootstrapClassOrNull方法查找启动类加载器是否加载此类,如果有加载则返回;如果没有加载则调用findClass方法。
    4. 递归的过程中如果有一处得到了Class,那么将返回此Class

    光说不练假把式,还是来举两个栗子吧

    1. 启动类加载器加载类

    测试代码:

    System.out.println(Provider.class.getClassLoader());
    

    接着将ClassLoader中的findClass设置断点,调试。执行结果如下:

    第一次
    AppClassLoader

    可以看到,第一次执行的时候是AppClassLoader进行loadClass方法的调用。接着进入parent.loadClass方法中:

    parent.loadClass
    ExtClassLoader

    接着调用了ExtClassLoader中的loadClass方法,我们知道其父类加载器不存在,所以执行findBootstrapClassOrNull方法:

    findBootstrapClassOrNull
    因为我现在挑选的是启动类加载器加载的类,所以这里面返回值不为空,接着就把此值返回给ExtClassLoaderExtClassLoader又把值返回给AppClassLoader,最终将值返回,整个过程结束。

    2. 应用程序类加载器

    测试代码:

    System.out.println(UserModel.class.getClassLoader());
    

    接着将ClassLoader中的findClass设置断点,调试。其查找过程和上面一致,这里不多说,这里需要知道的是此时findBootstrapClassOrNull方法返回值为null,接着会调用findClass方法:

    ExtClassLoader findClass
    ExtClassLoader
    ExtClassLoader中查找UserModel没有找到,返回结果null,紧接着就会调用AppClassLoaderfindClass方法:
    AppClassLoader findClass
    AppClassLoader
    通过defineClass方法最终获取到UserModel类,并将结果返回。

    破坏双亲委派机制的自定义类加载器

    双亲委派机制是建立在不重写loadClass流程的基础上,如果某一个自定义类加载器重写了loadClass方法,并将其流程改变,那么所谓的双亲委派机制也就消失了。下面的自定义类加载器破坏了双亲委派机制:

    public class CustomClassLoader extends ClassLoader {
    
        private String classPath;
    
        public CustomClassLoader(String classPath) {
            this.classPath = classPath;
        }
        // 重写了loadClass方法,不用去查找是否加载,如果类文件存在,直接返回所需类
        // 否则按照原方式进行
        @Override
        public Class<?> loadClass(String name) throws ClassNotFoundException {
            File file = new File(classPath + name.replace(".", "\\") + ".class");
            if (file.exists()) {
                try {
                    InputStream is = new FileInputStream(file);
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
    
            return super.loadClass(name);
        }
    
    }
    // 测试代码
    private static void test() {
        CustomClassLoader customClassLoader = new CustomClassLoader("C:\\Users\\B-0137\\Desktop\\");
        try {
            Class<?> userModel = customClassLoader.loadClass("com.nick.model.UserModel");
            Object o = userModel.newInstance();
            System.out.println(o);
            System.out.println(o instanceof UserModel);
            IUser iUser = (IUser) o;
            iUser.test();
    
            Class<?> mC = Main.class.getClassLoader().loadClass("com.nick.model.UserModel");
            Object mainO = mC.newInstance();
            System.out.println(mainO);
            System.out.println(mainO instanceof UserModel);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }
    

    测试结果:

    com.nick.model.UserModel@7f31245a
    false
    测试
    com.nick.model.UserModel@6d6f6e28
    true
    

    在这也能看出通过破坏双亲委派机制可以由不同的类加载器加载相同的类,但是他们并不相等——类加载器不同

    保持双亲委派机制的自定义类加载器

    其实想要保持双亲委派机制很简单:只需要在自定义类加载器的时候重写findClass方法即可
    自定义类加载器这里省略,就是重写了findClass方法,其他代码没变。测试代码:

    private static void test() {
        CustomClassLoader customClassLoader = new CustomClassLoader("C:\\Users\\B-0137\\Desktop\\");
        try {
            Class<?> userModel = customClassLoader.loadClass("com.nick.model.UserModel");
            Object o = userModel.newInstance();
            System.out.println(o);
            System.out.println(o instanceof UserModel);
            System.out.println(userModel.getClassLoader());
            IUser iUser = (IUser) o;
            iUser.test();
            System.out.println();
    
            Class<?> mC = Main.class.getClassLoader().loadClass("com.nick.model.UserModel");
            Object mainO = mC.newInstance();
            System.out.println(mainO);
            System.out.println(mainO instanceof UserModel);
            System.out.println(mC.getClassLoader());
            System.out.println();
            
            Class<?> userModel2 = customClassLoader.loadClass("com.nick.model.UserModel2");
            Object o2 = userModel2.newInstance();
            System.out.println(o2);
            System.out.println(userModel2.getClassLoader());
            IUser iUser2 = (IUser) o;
            iUser2.test();
    
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }
    

    测试结果:

    com.nick.model.UserModel@677327b6
    true
    sun.misc.Launcher$AppClassLoader@18b4aac2
    测试
    
    com.nick.model.UserModel@14ae5a5
    true
    sun.misc.Launcher$AppClassLoader@18b4aac2
    
    com.nick.model.UserModel2@135fbaa4
    com.nick.classloader.CustomClassLoader@7f31245a
    测试
    

    我们用自定义的类加载器去加载外部的一个和项目中同名的类,结果发现其是由应用程序类加载器加载,那么可以说明自定义类加载器重写findclass方法保持了双亲委派机制。

    结尾

    作为开年的第一篇文章,洋洋洒洒写了好多字。从论据到论点,详详细细全部写完。啰哩啰唆说了一大堆,结果还没进入正题(热修复)。这篇文章主要是为热修复打下些基础,下一篇将会讲述基于类加载器原理实现的热修复以及如何实现。
    最后上个美女养养眼吧~

    美女

    相关文章

      网友评论

        本文标题:Android 简单热修复(上)——Java类加载器

        本文链接:https://www.haomeiwen.com/subject/gjornxtx.html