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<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

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

推荐阅读更多精彩内容