Android热修复原理简介

Android热修复原理简介

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

闲话少说,今天准备写一篇关于Android热修复的东西

热修复四大框架

首先我们来对看一下主流框架对于热修复的对比图,了解一下各大厂商用的框架对比。热补丁方案有很多,其中比较出名的有腾讯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 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;
}

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

public Class findClass(String name, List 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热修复原理简介)