美文网首页
Android热修复原理简介

Android热修复原理简介

作者: yosen | 来源:发表于2020-03-17 21:24 被阅读0次

    Android热修复原理简介

    今天看到塞尔维亚总统在全国电视直播中说到,只有中国才能救我们的时候,作为中国人的那种骄傲油然而生,很幸运能见证中国的崛起和强大,这才是大国当担的样子。

    闲话少说,今天准备写一篇关于Android热修复的东西,部分内容来自 享学课堂lance老师

    热修复四大框架

    首先我们来对看一下主流框架对于热修复的对比图,了解一下各大厂商用的框架对比。热补丁方案有很多,其中比较出名的有腾讯Tinker、阿里的AndFix、美团的Robust以及QZone的超级补丁方案,下面是他们的对比

    热修复框架对比图

    腾讯Tinker:Tinker通过计算对比指定的Base Apk中的dex与修改后的Apk中的dex的区别,补丁包中的内容即为两者差分的描述。运行时将Base Apk中的dex与补丁包进行合成,重启后加载全新的合成后的dex文件。

    特点:重启生效、反射、类加载、DexDiff

    QQ的Qzone:QQ空间基于的是dex分包方案。把BUG方法修复以后,放到一个单独的dex补丁文件,让程序运行期间加载dex补丁,执行修复后的方法。如何做到这一点?在Android中所有我们运行期间需要的类都是由ClassLoader(类加载器)进行加载。因此让ClassLoader加载全新的类替换掉出现Bug的类即可完成热修复。

    特点:重启生效、反射、类加载

    美团Robust:对每个函数都在编译打包阶段自动的插入了一段代码。类似于代理,将方法执行的代码重定向到其他方法中。

    特点:即时生效、注解、插桩、代理

    阿里AndFix:在native动态替换java层的方法,通过native层hook java层的代码。

    特点:即时生效、不能替换类,只是通过改变Native层的指针改变所指向的方法,从而完成对方法的修复

    以上是各大平台使用热修复方案的优缺点,有些地方可能有些难以理解,这篇文章将着重介绍Qzone的原理和具体实现,其它方案读者可以自行研究,此处只做简单的介绍。

    QQ空间Qzone原理

    在介绍Qzone的实现原理之前,需要向大家介绍这么几个知识点:

    1. 类加载机制 classloader的原理

      我们知道任何一个类的class对象都会对应一个classloader,表示该类被哪个类加载器加载,Android原生api为我们提供了二种ClassLoader的抽象子类,分别为BootClassLoader,BaseClassLoader

      BootClassLoader用于加载Android Framework层的class文件,例于Activity.class等等

      BaseDe'xClassLoadexer下面又有两个子类,PathClassLoader,DexClassLoder

      PathClassLoader用于加载自己写的类,或者第三方库里面的类,包括android自己开发的第三方库

      DexClassLoder 和PathClassLoader一样,都是用来加载class文件

      其实两者并没有太大区别,只是构造方法不同而已,谷歌的意思是系统的类用pathclassloader,而我们用户自己写的类用DexClassLoder,但其实两者可以互相替换使用,只不过DexClassLoder比pathclassloader的构造方法多了一个参数,而这个参数只是用来保存我们的odex文件的目录,且在android更高的版本,这个参数也被弃用,被统一保存到系统的目录中。

      这些类加载器有一个共同的特性,在加载完一个类的class文件以后,不会再去加载相同的class文件,而我们就是利用这种机制,去实现热修复。

      在应用程序启动的时候,所有类的class文件,会被添加到一个Element的数组中,classloader有序的遍历这个数组,当遇见加载过重复的类时,就不会再去加载,所以我们只要想办法,帮我们要修复的class文件添加到这个集合的最前面,也就完成了热修复功能。

    2. 双亲委托机制

    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) {
            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.
                c = findClass(name);
            }
        }
        return c;
    }
    

    这是classloader加载类时候的源码,findLoadedClass相当于缓存,如果之前加载过可以直接加载出来。假设我们程序重新启动,代码会执行到 c = parent.loadClass(name, false); 查看源码可知parent为classloader内部维护的一个成员变量classloader parent,这里优先让parent加载类,如果parent没有找到,自己再去找,其实这里面有点类似装饰者模式,我们思考一个问题,在这个内部维护的parent内部是不是也有一个相同的classloader ,然后在查找这个name的时候,又会委托parent内部维护的classloader 去做,直到找不到为止,就自己来找。我们把这种机制称之为双亲委托机制。永远先让父加载器加载。总结入下:

    某个类加载器在加载类时,首先将加载任务委托给父 - 类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务或者没有父类加载器时,才自己去加载。

    那么为什么会有这个机制呢,

    1、避免重复加载,当父加载器已经加载了该类的时候,就没有必要子ClassLoader再加载一次。且只有一个classLoader就能加载出来系统所有的class对象

    2、安全性考虑,防止核心API库被随意篡改。 (假设我创建一个String类,如果没有这种机制,回导致我们的String类把系统的String替换掉)

    掌握以上两点基础知识,我们再来看看classloader是如何去加载一个类的。我们已经了解了,如果我们自己写一个类是会被PathClassLoader加载的,所以parent.loadClass(name, false)是注定找不到我们要修复的类,然后我们看看findClass的逻辑。PathClassLoader没有实现这个方法,我们来看他的父类BaseDexClassLoader的findclass

    private final DexPathList pathList;
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        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;
    }
    

    在findclass里面,又是通过pathList来查找,所以我们可以继续查看pathList.finClass做了什么

    public Class<?> findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            Class<?> clazz = element.findClass(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }
    
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }
    

    在DexPathList内部,又是通过element来findClass,所以我们最终只要锁定Element这个数组即可。系统会把我们所有dex文件,加载到Element数组中,然后有序遍历,而我们要想给一个类打补丁,就必须要保证这个补丁类的dex文件在错误类dex文件之前加载,而实现步骤就是在这个数组最开始的位置插入这个打了补丁的dex文件即可。(因为数组大小固定,为了避免数组角标越界,我们需要替换这个数组而不是插入)

    所以总结一下,想要做到热修复,需要做到如下几步:

    1. 获取到当前应用的PathClassloader;

    2. 反射获取到DexPathList属性对象pathList;

    3. 反射修改pathList的dexElements
      3.1 把补丁包patch.dex转化为Element[] (patch)
      3.2 获得pathList的dexElements属性(old)
      3.3 patch+old合并,并反射赋值给pathList的dexElements

    问题:QQ空间兼容问题
    https://mp.weixin.qq.com/s?__biz=MzI1MTA1MzM2Nw==&mid=400118620&idx=1&sn=b4fdd5055731290eef12ad0d17f39d4a

    相关文章

      网友评论

          本文标题:Android热修复原理简介

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