QQ空间热修复原理和注意点

以下内容摘录自Android热修复学习之旅开篇——热修复概述
参考:
从Java类加载初始化到Android热修复

QQ空间热修复补丁技术

原理

QQ空间的热修复方案是基于dex分包的基础之上的,简单来说就是把bug方法修复之后,然后重新生成一个dex,从服务器下发以后,将其插入到dexElements前面,让虚拟机去加载修复后的方法。(关于dex分包相关内容请参考Android Dex分包方案和热补丁原理)如下图所示:

image.png

这里涉及到到ClassLoad的原理,当一个类被加载以后,如果后面再出现相同的类就不会再加载了。这就是补丁热修复最基本的原理。
但是采用这种方案,有一个明显的问题,那就是当两个调用关系的类不在同一个dex里时,就会产生异常报错。发生异常的原因是,在apk安装时,虚拟机会对classes.dex进行优化,变成odex文件,然后才会执行。在这个过程中,会进行类的verify的验证工作,如果调用关系的类都在同一个dex中的话就会被打上CLASS_ISPREVERIFIED的标志,然后才会写入odex文件。

如果使用这种方案时,必须要避免类被打上CLASS_ISPREVERIFIED标记,具体的做法就是在每一个类的构造函数中单独引用一个在另外dex中的类。

我们通过Android类加载基础之ClassLoder的分析已经知道,无论是PathClassLoader还是DexClassLoader最终调用的都是BaseDexClassLoader里的方法,而且我们加载完外部的n个dex以后会被转换为Element[]数组存储在DexPathList中。PathClassLoader和DexClassLoader的差别就是DexClassLoader可以加载外部的dex而PathClassLoader只能加载已经安装了的内部dex,其中PathClassLoader 在应用启动时创建,从 data/app/… 安装目录下加载 apk 文件
一个ClassLoader可以包含多个dex文件,每个dex文件就是一个Element,多个dex文件排列成一个有序的数组dexElements,当查找某个类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找到则直接返回,如果找不到则从下一个dex文件继续查找。
理论上,如果在不同的dex中有相同的类存在,那么会优先选择排在前面的dex文件的类。

image.png

所以QQ空间热修复方案正是基于ClassLoader的这个原理,把修复后的类打包到一个dex(path.dex)中去,然后把这个dex插入到Elements的最前面去。


image.png

步骤

1.获取到当前引用的ClassLoader
2.通过反射获取到它的DexPathList属性对象pathList
3.通过反射调用pathList的dexElements方法把path.dex转化为Element数组
4.两个Element数组进行合并,把path.dex放到数组的最前面去
5.加载合并后行的Element数组,达到修复bug的目的

image.png

缺点

1.不支持即时生效,必须通过重启才能生效
2.path.dex是用来存储修复的类,应用启动时,就要加载path.dex,当修复的类到了一定的数量的时候,就会出现加载时间过长,造成应用启动卡顿
3.在ART模式下,如果类结构发生了改变,就会出现内存错乱。为了解决这个问题,就必须把所有相关的调用类、父类、子类等等全部加载到path.dex中,导致补丁包异常的大,进一步增加了应用的启动时间

CLASS_ISPREVERIFIED的问题

采用dex分包方案会遇到的问题,也就是CLASS_ISPREVERIFIED,简单来说就是:在虚拟机启动的时候,当verify选项被打开的时候,如果static方法、private方法、构造方法等,其中的直接引用(第一层关系)到的类都在同一个dex文件中,那么该类就会被打上CLASS_ISPREVERIFIED标志。
注意:这里避免被打上CLASS_ISPREVERIFIED标志的类似引用者类,而不是被引用者。也就是说假设你的app里面有个类叫做AClass,在其内部引用了BClass。发布过程中发现BClass有编写错误,那么想要发布一个新的BClass类,那么你就要阻止AClass这个类被打上CLASS_ISPREVERIFIED标志。也就是说,你在生成apk之前,就需要阻止相关类打上CLASS_ISPREVERIFIED的标志了。如何阻止,简单来说,就是让AClass在构造方法中,去直接引用别的dex文件中的类即可,比如:C.dex中的CClass。
总结:
1.动态改变BaseDexClassLoader对象间接引用的 dexElements
2.在app打包的时候,阻止相关类去打上CLASS_ISPREVERIFIED标志。

关键代码实现

采用QQ空间的热修复方案而实现的开源热修复框架就是HotFix,说到了使用dex分包方案会遇到CLASS_ISPREVERIFIED问题,而解决方案就是在dx工具执行之前,将所有的class文件,进行修改,再其构造中添加System.out.println(dodola.hackdex.AntilazyLoad.class),然后继续打包的流程。注意:AntilazyLoad.class这个类是独立在hack.dex中。

dex分包方案实现需要关注以下问题:

1.如何解决CLASS_ISPREVERIFIED问题
2.如何将修复的.dex文件插入到dexElements的最前面

CLASS_ISPREVERIFIED问题

在老版的Gradle中我们通过以下代码关联task并执行插件来动态插入代码

image.png

image.png

PatchClass中的代码比较简单我就不分析了,主要用到了javassist技术,感兴趣的朋友可以去查找相关的资料。
Gradle的更新速度很快,当我们的AndroidStudio升级以后,系统已经提供了更好的api来操作代码在编译过程中的回调,这就是Transform api,该兴趣的小伙伴可以参考我以前写的文章:编写最基本的Gradle插件
经过上面的代码,我们已经解决了CLASS_ISPREVERIFIED的问题

将修复的.dex文件插入dexElements

寻找class其实就是遍历dexElements,然后我们的AntilazyLoad.class其实并不包含在apk的classes.dex中,并且根据上面描述的需要,我们需要将AntilazyLoad.class这个类打成独立的hack_dex.jar,注意不是普通的jar,而是经过dx工具进行转化后的。具体做法如下:

jar cvf hack.jar dodola/hackdex/*
dx --dex --output hack_dex.jar hack.jar

还记得之前我们将所有的类的构造方法中都引用了AntilazyLoad.class,所以我们需要把hack_dex.jar插入到dexElements,而在hotfix中,就是在Application中完成这个操作的。

        File dexPath = new File(getDir("dex", Context.MODE_PRIVATE), "hackdex_dex.jar");
        //从assets中读取文件被写入到指定文件中去
        Utils.prepareDex(this.getApplicationContext(), dexPath, "hackdex_dex.jar");
        //通过反射去修改dexElements数组
        HotFix.patch(this, dexPath.getAbsolutePath(), "dodola.hackdex.AntilazyLoad");
        try {
            this.getClassLoader().loadClass("dodola.hackdex.AntilazyLoad");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

HotFix#patch

public static void patch(Context context, String patchDexFile, String patchClassName) {
        if (patchDexFile != null && new File(patchDexFile).exists()) {
            try {
                if (hasLexClassLoader()) {
                    injectInAliyunOs(context, patchDexFile, patchClassName);
                } else if (hasDexClassLoader()) {
                    injectAboveEqualApiLevel14(context, patchDexFile, patchClassName);
                } else {
                    injectBelowApiLevel14(context, patchDexFile, patchClassName);
                }
            } catch (Throwable th) {
            }
        }
    }

根据不同的平台然后分别注入到不同平台下的dexElements中。这里我们只分析api
14 以上的平台,其他平台大家自己去分析。其实原理都是差不多的。

private static void injectAboveEqualApiLevel14(Context context, String dexPath, String dexClassName)
        throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        //得到当前的PathClassLoader
        PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
        //将老的dexElements和pathDexElements进行组合生出新的dexElements
        Object a = combineArray(getDexElements(getPathList(pathClassLoader)),
            getDexElements(getPathList(
                new DexClassLoader(dexPath, context.getDir("dex", 0).getAbsolutePath(), dexPath, context.getClassLoader()))));
        //拿到DexPathList对象
        Object a2 = getPathList(pathClassLoader);
        //将DexPathList实例中的dexElements成员替换为合并后的dexElements
        setField(a2, a2.getClass(), "dexElements", a);
        //加载指定的类
        pathClassLoader.loadClass(dexClassName);
    }

好了,经过上面的分析,我们已经将hack_dex.jar成功的插入到dexElements的最前面了,而补丁插入的过程也和hack_dex.jar的插入流程是一致。

总结

QQ热修复步骤

1.发现某个类中存在bug
2.创建一个相同的类解决bug,并通过javassist技术解决CLASS_ISPREVERIFIED的问题,然后下发到指定的客户端
3.app启动时会创建PathClassLoader,扫描指定目录下的dex文件并保存到DexPathList的dexElements数组中。
4.Application#onCreate中查找是否有path.dex文件,如果没有则通过网络下载,保存到assets中,然后拷贝到app的指定目录下。如果存在path.dex,则创建DexClassLoader加载它,然后得到它的dexElements数组,与PathClassloader中的dexElements数组进行合并(插入到头部),通过反射将新生成的数组注入到原来的dexElements中,从而完成bug类的替换。
5.当我们在初始化原先的bug类的时候,会从新生成dexElements中查找,由于双亲委托,当我们已经找到了新类的时候,他就不会再去查找原先老的bug类,所以此时的对象旧已经完成了bug的修复。

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