热修复 -- Sophix & Tinker
一、Sophix & Tinker
Sophix & Tinker二、使用
Sophix使用
Tinker使用
三、原理
代码修复
代码修复有两大主要方案,一种是阿里系的底层替换方案,另一种是腾讯系的类加载方案。这两类方案各有优缺点:
- 底层替换方案限制颇多,但时效性最好,加载轻快,立即见效。
- 类加载方案时效性差,需要重新冷启动才能见效,但修复范围广,限制少。
底层替换方案(热替换代码修复:Sophix):
底层替换方案是在已经加载的类中通过动态修改native指针来直接替换原有方法,是在原有类的基础上进行修改的,因而无法实现对原有类进行方法和字段的增减,因为这样将破坏原有类的结构。
类加载方案(冷启动代码修复:Tinker Sophix):
类加载方案的原理是在App重新启动后让ClassLoader去加载新的类。因为在App启动到一半的时候,所有需要发生变更的类已经被加载过了,在Android系统上是无法对一个类进行卸载的。如果不重启,原有的类还在虚拟机中,就无法加载新类。因此,只有在下次重启的时候,在还没有运行到业务逻辑之前抢先加载补丁中的新类,这样后续访问这个类时,就会被解析为新类,从而达到热修复的目的。
-
Tinker
原理:通过差量的方式得到patch.dex,然后将patch.dex与应用的旧dex文件合并成一个新dex文件,合成的全量dex文件插入到dexElements的第一个位置,dexElements里面的每个Element实际上就是dex文件,classLoader在启动时,会按顺序加载dex文件,使修复类所在的全量dex包被优先加载,从而完成替换。
优点:自研dex差异算法,补丁包很小。
缺点:dex合并内存消耗在vm heap上,容易OOM,最后导致dex合并失败。
Tinker类加载方案 -
Sophix
Art虚拟机:默认支持多dex加载,虚拟机会优先加载命名为classes.dex的文件。Sophix将补丁文件命名为classes.dex,并对原有dex文件进行排序,这样Art虚拟机就会先加载补丁文件,后续加载的同类名的类会被忽略。
Sophix类加载方案-Art虚拟机
Dalvik虚拟机:默认只加载classes.dex,其他dex则被忽略,Sophix需要一个全量dex。Tinker是采用自研的dex差异算法,从方法和指令的维度进行dex合成,但dex合成过程发生在虚拟机堆内存上,修复的成功率受性能影响。而Sophix换了一种思路,从类的维度对照补丁包中出现的类,在原有包中做删除操作。
Sophix类加载方案-Dalvik虚拟机
资源修复
Instant Run:
目前市面上的很多资源修复方案基本上都是参考了Instant Run的实现。Instant Run中的资源热修复分为两步:
1.构造一个新的AssetManager,并通过反射调用addAssetPath函数,然后把完整的新资源包加载到AssetManager中。这样就得到了一个含有所有新资源的AssetManager。
2.找到所有之前引用到原有AssetManager的地方,通过反射,把引用出替换为新的AssetManager。
Sophix:
没有直接参考Instant Run的技术,构造了一个package id为0x66的资源包,这个包里只包含需要改变的资源项(原有包里面没有的新增资源,以及原有内容发生了改变的资源),然后直接在原有AssetManager中通过addAssetPath函数添加这个包就可以了。由于补丁包的package id为0x66,不与目前已经加载的地址为0x7f的包冲突,因此直接加载到已有的AssetManager中就可以直接使用了。整个资源替换的方案优势如下:
- 不修改AssetManager的引用处,替换资源更快更完全。
- 不必下发完整包,补丁包中只包含变动的资源。
- 不需要在运行时合成完整包,不占用运行时计算和内存资源。
唯一有个需要注意的地方就是,因为对新的资源的引用是在新代码中,所有资源修复是需要代码修复的支持的。也因此所有资源修复方案必然是附带代码修复的。
so库修复
Java API提供以下两个接口加载一个so库:
- System.loadLibrary(String libName):传进去的参数是so库名称,表示的so库文件,位于APK压缩文件中的libs目录,最后复制到APK安装目录下。
- System.load(String pathName):传进去的参数是so库在磁盘中的完整路径,加载一个自定义外部so库文件。
so库的修复本质上是对native方法的修复和替换。
Sophix:
采用的是类似类修复反射注入方式。把补丁so库的路径插入到nativeLibraryDirectories数组的最前面,就能够达到加载so库的时候是补丁so库的目录从而修复Bug的目的。采用这种方案,完全由Sophix在启动期间反射注入补丁中的so库,重启生效。