目录
1什么是热修复
2.为什么需要热修复
3.目前存在的解决方案
4.各自优缺点
5.如何应用和取舍
1.什么是热修复
传统发版流程。
在这个流程当中发版重新上线需要用户重新下载覆盖安装无法做到全面替换,而且一次安装包较大用户安装的成本较高。
替代方案之一:Hybird.
缺点是采用H5以后性能与原生相比相差甚远。
更优的解决方案:热修复.
热修复的出现将传统发版流程修改成如下流程:
热修复流程在修复bug以后采用无感知方式直接推送给所有用户无需安装即可修复,有点是覆盖面广修复成功率高,且比下载一个apk安装成本要低。
2.为什么需要热修复
上面实际上已经解答了这个问题。
3.目前存在的解决方案:
阿里:andFix HotFix sophix
腾讯:Tinker qq空间热补丁方案
美团:Robust
根据不同技术方案实现重新对上述方案进行分类:
底层替换方案:AndFix
multidex方案:Tinker qq空间热补丁方案,nuwa等
基于instant run方案:robust.
3.1底层替换方案:
AndFix主要是在native层使用指针替换的方式替换bug方法,通过自定义注解找到需要修复的类和方法。首先类加载其方法都放在方法区,在native层中有相应的结构体描述对应的方法和执行逻辑,一旦某个方法出现bug,可以新建一个类然后把修复后的方法放进来,把原来的那个类的方法的指针指向新的类方法就完成了bug修复。
Sophix:是在AndFix进一步优化产生的,由于不同厂商的底层结构体不同,按照AndFix的方式很难兼容所有平台。而Sophix很巧妙的解决了这个问题,结构体的替换例如修改指针,改访问权限其前提是Android手机的结构体与AndFix的结构体要一致才能才能完好的进行替换,所以Sophix另辟蹊径采用整体替换的方式,
同包名访问权限问题采用classloader进行替换将新的替换为老的即可。
QQ超级补丁方案:
基于Android dex分包机制完成
QQ超级补丁的原理基于Android dex加载原理,新修复的类放在patch.dex中插桩到dexElements最前面,就算是完成了bug热修复,虚拟机优先加载patch.dex中的qzone.class,为解决不在不同一个dex的两个调用类关系。也就是防止类被打上CLASS_ISPREVERIFIED标志,设计一个AntilazyLoad类,然后在所有类的构造方法中加入
if (ClassVerifier.PREVENT_VERIFY) {
System.out.println(AntilazyLoad.class);
}
同时AntilazyLoad类会单独打包成hack.dex,这样所有的类都会引用不同的dex
修复的步骤为:
1.获取当前用的Classloader,即为BaseDexClassloader,目的会为了获取其pathlist
2.分别获取程序和补丁的pathList得到两者的dexElements然后进行合并,将补丁放到最前面
Tinker:
dalvik由于插桩导致耗时,性能损耗大,在art状态下修改方法和filed会导致内存指针异常,为了解决这个问题我们需要将修改了变量、方法以及接口的类的父类以及调用这个类的所有类都加入到补丁包中。这可能会带来补丁包大小的急剧增加。
全量替换新dex,对比新旧dex,在运行时将path.dex与旧dex还原成新的dex,DexDiff的细粒度是dex中每一项
缺点:
占用Rom体积;这边大约是你修改Dex数量的1.5倍(dexopt与dex压缩成jar)的大小。
一个额外的合成过程;虽然我们单独放在一个进程上处理,但是合成时间的长短与内存消耗也会影响最终的成功率。
难点:
Dex格式复杂;Dex大致分为像StringID,TypeID这些Index区域以及使用Offset的Data区域。它们有大量的互相引用,一个小小的改变可能导致大量的Index与Offset变化;
dex2opt与dex2oat校验;在这两个过程系统会做例如四字节对齐,部分元素排序等校验,例如StringID按照内容的Unicode排序,TypeID按照StringID排序...
低内存,快速;这要求我们对Dex每一块做到一次读写,无法像baksmali与dexmerge那样完全结构化。
robust
遇到的难点:混淆以及super函数
缺点:太过复杂。无法做到资源.so库替换和AndFix类似也是方法替换
1.DexClassLoader和PathClassLoader.
DexClasLoader加载jar,dex以及apk的classes.dex文件,可以执行非安装程序代码
PathClassLoader是Android使用这个类作为系统和应用类的加载器。
#DexPathList
public Class findClass(String name) {
for(Element element : dexElements) {
DexFile dex = element.dexFile;
if(dex !=null) {
Class clazz = dex.loadClassBinaryName(name, definingContext);
if(clazz !=null) {
return clazz;
}
}
}
return null;
}
将dex转化成odex的过程中,会在DexVerify.cpp进行校验,验证如果直接引用到的类和clazz是否在同一个dex,如果是,则会打上CLASS_ISPREVERIFIED标志,需要阻止打上这个标记。
instant run原理
ART所使用的AOT(Ahead-Of-Time)编译,在应用首次安装时,字节码预编译成机器码存储在本地,也就是说在程序运行前编译。而Dalvik是典型的JIT(Just_In_Time),此模式下,应用每次运行的时候,字节码都需要即时编译器转换为机器码再执行,也就是在程序运行时编译。因此在App运行时,ART模式相对于Dalvik省去了解释字节码的过程,占用内存也相应减少,进而提高App。
public int getIndex() {
return 105;
}
public static ChangeQuickRedirect changeQuickRedirect;
public long getIndex() {
if(changeQuickRedirect != null) {
//PatchProxy中封装了获取当前className和methodName的逻辑,并在其内部最终调用了changeQuickRedirect的对应函数
if(PatchProxy.isSupport(new Object[0], this, changeQuickRedirect, false)) {
return ((Long)PatchProxy.accessDispatch(new Object[0], this, changeQuickRedirect, false)).longValue();
}
}
return 100L;
}
public class PatchesInfoImpl implements PatchesInfo { public ListgetPatchedClassesInfo() { ListpatchedClassesInfos = new ArrayList();
PatchedClassInfo patchedClass = new PatchedClassInfo("com.meituan.sample.d", StatePatch.class.getCanonicalName());
patchedClassesInfos.add(patchedClass);
return patchedClassesInfos;
}
}
public class StatePatch implements ChangeQuickRedirect {
@Override
public Object accessDispatch(String methodSignature, Object[] paramArrayOfObject) {
String[] signature = methodSignature.split(":");
if (TextUtils.equals(signature[1], "a")) {//long getIndex() -> a
return 106;
}
return null;
}
@Override
public boolean isSupport(String methodSignature, Object[] paramArrayOfObject) {
String[] signature = methodSignature.split(":");
if (TextUtils.equals(signature[1], "a")) {//long getIndex() -> a
return true;
}
return false;
}
}
直接用DexClassLoader加载修复包,然后用loadClass方法加载修复类,new出新对象,在用反射把这新的修复对象设置到指定类的changeQuickRedirect静态变量中即可。
第一次编译apk:
1.把Instant-Run.jar和instant-Run-bootstrap.jar打包到主dex中
2.替换AndroidManifest.xml中的application配置
3.使用asm工具,在每个类中添加$change,在每个方法前加逻辑
4.把源代码编译成dex,然后存放到压缩包instant-run.zip中
app运行期:
1.获取更改后资源resource.ap_的路径
2.设置ClassLoader。setupClassLoader:
使用IncrementalClassLoader加载apk的代码,将原有的BootClassLoader → PathClassLoader改为BootClassLoader → IncrementalClassLoader → PathClassLoader继承关系。
3.createRealApplication:
创建apk真实的application
4.monkeyPatchApplication
反射替换ActivityThread中的各种Application成员变量
5.monkeyPatchExistingResource
反射替换所有存在的AssetManager对象
6.调用realApplication的onCreate方法
7.启动Server,Socket接收patch列表
有代码修改时
1.生成对应的$override类
2.生成AppPatchesLoaderImpl类,记录修改的类列表
3.打包成patch,通过socket传递给app
4.app的server接收到patch之后,分别按照handleColdSwapPatch、handleHotSwapPatch、handleResourcePatch等待对patch进行处理
5.restart使patch生效