热修复技术概要

市面上热门热修复框架对比

方案对比 Sophix Tinker
Dex修复 同时支持及时生效和冷启动修复 冷启动修复
资源更新 差量包,不用合成 差量包,需要合成
SO库更新 插桩实现,开发透明 替换接口,开发不透明

代码修复

底层替换方案(热部署 )

这个方案由阿里的Anfix首先提出,在已经加载了的类中直接在native层替换原有的方法,是在原来的类上做修改的。

每个Java方法在Android虚拟机(4.4以下用的是dalvik虚拟机,4.4以上用的是art虚拟机)中都对应着一个ArtMethod指针,ArtMethod记录了这个方法的所有信息,包括所属类、访问权限、代码执行地址等等。通过Method对象得到底层Java函数对应ArtMethod的真实地址,替换ArtMethod结构体中的字段。

因此,当把一个旧方法的所有成员字段都替换成新方法后,执行时所有数据就可以保持和旧方法的一致。这样在所有执行到旧方法的地方,会取得新方法的执行入口、所属class、方案索引以及dex信心,然后像调用旧方法一样顺滑地执行到新方法的逻辑。

兼容问题

但是由于Android代码是开源的,所有的厂商都可以对代码进行改造,而Andfix里ArtMethod的结构是根据公开的Android源码写死的。如果厂商对这个ArtMethod结构体进行修改,就有兼容问题了。

突破底层结构差异

把ArtMethod作为整体进行替换,忽略底层ArtMethod结构的差异,对于所有的Android版本都不再需要区分。

同包名下的权限问题

补丁中的类在访问同包名下的类时,会报出访问权限异常。

虽然补丁中替换的类与app中原来的类是在同一个包名,但是替换的类是补丁包的Classloader加载的,与原先的包不在同一个Classloader,这样就导致两个类无法被判断为同包名。

知道了原因,只需要利用反射把新类的classLoader设置为原来类的即可。

及时生效所带来的限制

这方案只适合方法的替换。而对补丁类里面存在方法增加和减少,以及成员变量字段的增加和减少情况都不适用。

原因是这样的,一旦补丁类中出现了方法的增加和减少,就会导致这个类以及整个Dex的方法数的变化。方法数的变化伴随着方法索引的变化,这样在访问方法时就无法正常的索引到正确的方法了。如果字段发生了增加和减少,和方法变化的情况一样。

不过新增一个完整的、原来类中没有的新类是可以的,这个不受限制。

类加载方案(冷启动修复)

原理

利用Android的ClassLoader——DexClassLoader的加载过程,其中一个环节就是调用DexPathList的findClass方法。

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

Element内部封装了DexFile,DexFile用于加载dex文件,因此每个dex文件对应一个Element.多个Element组成看有序的Element数组dexElements。

findClass()方法里遍历dexElements数组,调用findClass方法,其方法内部会调用DexFile的loadClassBinaryName方法查找类。如果在Element中查找到该类就返回,如果没有就在下一个Element中进行查找。根据上面的查找过程,我们将有bug的类A.class进行修改,再将A.class打包成为dex的补丁包Patch.jar,放在Element数组dexElements的第一个元素,这样会首先找到Patch.dex中的A.class去替换之前存在bug的A.class,排在数组后面的dex文件中存在bug的A.class根据ClassLoader的双亲委托模式就不会被加载,这就是类加载方案。

业界方案对比
~ QQ空间 Tinker
原理 为了解决Dalvik下Unexpected dex problem异常而采用的插桩的方式,单独放一个帮助类在独立的dex中让其他类调用,阻止了类被打上 CLASS_ISPREVERIFIED标志从而规避问题的出现。最后加载补丁dex得到dexFile对象作为参数构造一个Element对象插入到dexElements数组的最前面 提供差量包,整体替换dex的方案。差量的方式给出patch.dex,然后将patch.dex与应用的classes.dex合并成一个完整dex,完整dex加载得到的dexFile对象作为参数构建一个Element对象然后整体替换掉旧的Elements数组
优点 没有合成整包,产物比较小,比较灵活 自研dex差异算法,补丁包很小,dex merge成完整dex,Dalvik不影响类加载性能,Art下也不存在必须包含父类/引用类的情况
缺点 Dalvik下影响加载性能,Art下类地址写死,必须包含父类/引用,最后补丁包很大 dex合并内存消耗在vm heap上,容易OOM,最后导致dex合成失败

利用google已经开源的dexmerge方案,把补丁dex和原dex合并成一个完整的dex似乎可行,但仅仅这样还是不够的,多dex下如果dexmerge会有65535方法数超过异常,dexmerge会导致内存风暴,内存不足情况下很容易失败。

新的全量Dex方案

我们可以这样考虑,既然补丁中已经有变动的类了,那只要在原先基线包里的dex里面,去掉补丁中也有的class.这样,补丁+去除了补丁类的基线包,不就等于新app中所有类吗?

Google的multi-dex是把一个app里面的所有类拆分到classes.dex、classes2.dex...之中,而每个dex中都只包含了部分的类的定义,但单个dex也是可以加载的,因为只要把所有的dex都加载进去,本dex中不存在的类就可以在运行期间在其他dex中找到。

因此同理,在基线包dex里面在去掉了补丁中class后,原先需要发生变更的旧的class就被消除了,基线包dex里只包含不变的class。而这些不变的class要用到补丁中新的class会自动地找到补丁dex,补丁包中的新class在需要用到不变的class会自动去找基线包dex的class。这样的话,基线包里面不使用补丁类的class仍然可以按照原来的逻辑做odex,最大的保证dexopt的效果。

那么,怎么在基线包里去掉补丁包中包含的所有类?

先看下dex文件结构


dex.png
数据名称 解释
header dex文件头部,记录整个dex文件的相关属性
string_ids 字符串数据索引,记录了每个字符串在数据区的偏移量
type_ids 类似数据索引,记录每个类型的字符串索引
proto_ids 原型数据索引,记录了方法声明的字符串,返回类型字符串,参数类型
field_ids 字段数据索引,记录了所属类、类型以及方法名
method_ids 类方法索引,记录方法所属类名,方法声明以及方法名等信息
class_defs 类定义数据索引,记录指定类各类信息,包括接口,超类,类数据偏移量
data 数据区,保存了各个类的真实数据
link_data 连接数据区

这里打算去除dex里的Class,因此我们关心的自然是这里面的class_defs。我们要做的只需要移除定义的入口,对于Class的具体内容不进行删除,这样可以最大可能的减少offset的修改。

对于Application的处理

Application是整个app的入口,因此,在进入到替换的完整dex之前,一定会通过application的代码,因此,application必然是加载在原来的老dex里面的。只有在补丁加载后使用的类,会在新的完整的dex里面找到。

因此,在加载补丁后,如果application类使用其他在新dex里的类,由于不在同一个dex里的类,由于不在同一个dex里,如果application被打上了pre-verified标志,这时就会抛异常。解决方法很简单:只要在jni层把CLASS_ISPREVERIFIED标志清除掉就行了。

但是在开发中,这种清除标志的方案并非一帆风顺,如果这个入口Application是没有pre-verified的,反而有更大的问题。

这个问题是Dalvik虚拟机如果发现某个类没有pre-verified,就会在初始化这个类时做verify操作,这将扫描这个类的所有代码,在扫描过程中对这个类代码里使用的类都进行dvmOptResolveClass操作。

而这个dvmOptResolveClass这是罪魁祸首,它会在Resolve的时候对使用到的类进行初始化,而这个逻辑发生在Application类初始化的时候。此时补丁还没进行加载,所以就会提前加载到原始dex中的类。接下来补丁加载完毕后,这些已经加载的类如果用到了新dex中的类,并且是pre-verified时就会报错。

这里最大的原因在于我们无法把补丁加载提前到dvmOptResolveClass之前,因为在一个app的生命周期里,没有可能到达比入口Application初始化更早的时期了。

而这个问题常见于多dex情形,当存在多dex时,无法保证Applicatiion用到的类和它处于同一个dex中,一般就不会有这个问题。

多dex情况下要想解决这个问题,有两个办法:

  • 第一,让Application用到的所有非系统类都和application位于同一个dex,这样就可以保证pre-verified标志被打上,避免进入dvmOptResolveClass,而在补丁加载完之后,我们再清除pre-verified标志,使得接下来使用其他类也不会报错。

  • 第二,把Application里面除了热修复框架以外的其他代码都剥离开,单独提出放到一个其他类中,这样使得Application不会直接用到过多非系统类,这样,保证这个单独拿出来的类和Application处于同一个类dex的概率比较大。如果想更保险,Application可以采用反射的方式访问这个单独类,这样就彻底把Application和其他类隔离。

资源修复

Instant Run 方案

  1. 创建新的AssetManager,通过反射调用addAssetPath方法加载外部的资源,这样新创建的AssetManager就含有所有外部资源

  2. 找到所有之前引用到原有AssetManager的地方,通过反射,把引用处替换为新创建的AssetManager。

so库修复

so的修复主要是重新加载so,所以so库修复的基本原理就是加载so.

加载so主要是用到System类的load和loadLibrary方法。System的load方法传入的参数是so在磁盘的完整路径,用于加载指定路径的so.System的loadLibrary方法传入的是so的名称。

上面两种方式加载一个so库,实际上最后都调用nativeLoad这个native方法去加载so库,这个方法的参数 fileName: so 库在磁盘的完整路径名。

接口调用替换方案

sdk提供接口代换System.loadLibrary(String name),优先尝试加载指定目录下的补丁so,加载策略如下:

  • 如果存在则加载补丁so库而不是加载安装apk目录下的so库
  • 如果不存在补丁so,那么调用System.loadLibrary去加载安装apk目录下的so库。

优点:不需要对不同sdk进行兼容,因为所有的sdk版本都有System.loadLibrary这个接口

缺点:没法修复第三方包的so库

反射注入方案

利用反射注入方式,把补丁so路径插入到nativeLibraryDirectories(sdk>23 换成nativeLibraryPathElements)数组最前面就能达到so库的时候是补丁so库而不是原来so库的目录,从而达到修复的目的。

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