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

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

  • 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方法保持了双亲委派机制。

结尾

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

美女
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容