【关于tinker的详细接入请看我的这篇文章】https://blog.youkuaiyun.com/Checkiming/article/details/85223118
Why use thermal fixes?
我们平时遇到刚上线不久的项目APP的bug崩溃或者影响用户体验的话,如果按照通常做法,那就是程序猿加班搞定bug,然后测试,重新打包并发布。这样带来的问题就是成本高,效率低。于是,热修复就应运而生.
What is HotFix?
HotFix(热修复)是针对某一个具体的系统漏洞或安全问题而发布的专门解决该漏洞或安全问题的小程序,通常称为修补程序
传统
这时候我们通常的处理流程是:解决bug、重新打包App、测试、向各个应用市场和渠道换包、提示用户升级、用户下载、覆盖安装。有时候仅仅是为了修改了一行代码,也要付出巨大的成本进行换包和重新发布,而且你难道不站在用户的角度看看这件事?这件事对于用户体验难道不糟糕吗? ̄へ ̄
初级解决方式:
在移动 App 开发里,可以动态部署达到热修复。例如某页面出现了 Bug ,后台服务器修改配置为采用 React Native 显示此页面,而有 Bug 的 Native 页面就隐藏了。等到下一次版本更新发到线上渠道,再采用 Native 展示此页面。
热修复的驳论:
- 运行时修复不用等待App重新启动,要是需要重启的话,就是冷修复了
- 发到线上的客户端,是没法及时修改的,但是服务器可以随时控制
- Deposed 和 Fix 都不能全部适配,各自的方案都有自己的优缺点,classloader 可以发挥更好用途,局部的更新倒是采用 andfix 这样类型的方案,感觉比较方便。
###Android虚拟机原理初读 **
一、ClassLoader
Android中的虚拟机 Dalvik/ART VM 不同于Java的JVM,二者之间的区别 当然这不是今天所要谈及的重点,我们只需辨别Dalvik/ART VM 虚拟机加载类和资源也是要用到ClassLoader,不过Jvm通过ClassLoader加载的class字节码,而Dalvik/ART VM通过ClassLoader加载则是dex就够了。
Android中的类加载器ClassLoader**分为两种,PathClassLoader和DexClassLoader,两者都继承BaseDexClassLoader
PathClassLoader代码位于libcore\dalvik\src\main\Java\dalvik\system\PathClassLoader.java
DexClassLoader代码位于libcore\dalvik\src\main\java\dalvik\system\DexClassLoader.java
BaseDexClassLoader代码位于libcore\dalvik\src\main\java\dalvik\system\BaseDexClassLoader.java
其中PathClassLoader是用于加载系统类和应用类
DexClassLoader则是用来加载jar、apk、dex文件(加载jar、apk最终也是抽取里面的Dex文件进行加载。)
**二、热修复机制 **
利用dexElements的顺序来做文章,当一个补丁的patch.dex放到了dexElements的第一位,那么当加载一个bug类时,发现在patch.dex中,则直接加载这个类,原来的bug类可能就被覆盖了
PathClassLoader 代码
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
}
DexClassLoader代码
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
可以看出这两个ClassLoader类几行代码就调用了父类的构造函数
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
@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;
}
热修复方案比较
其中AndFix可能是接入最简单的一个(和Tinker命令行接入方式差不多),不过兼容性还是是有一定的问题的;QZone方案对性能会有一定的影响,且在Art模式下出现内存错乱的问题(其实这个问题我之前并不清楚,主要是tinker在MDCC上指出的);美团提出的思想方案主要是基于Instant Run的原理,目前尚未开源,不过这个方案我还是蛮喜欢的,主要是兼容性好。
这么看来,如果选择开源方案,tinker目前是最佳的选择,tinker的介绍有这么一句:
Tinker已运行在微信的数亿Android设备上,那么为什么你不使用Tinker呢?
目前较为热门的热修复技术有
阿里的Xposed、AndFix、QZone(ClassLoader)的方案、美团提出的思想方案(基于Instant Run的原理)以及腾讯的Tinker.
- 前两个方案思路极其相似,都是通过指针替换掉出Bug的方法、类,不同的是Xposed将需要替换的方法连接到hookedMethodCallback,以它来实现替换后方法的调配,而AndFix就比较简洁粗暴了,直接就是获取需要替换的方法指针,将指针指向修改之后的新的java代码.
- 相比之下,ClassLoader方案便较为巧妙的多了,他巧妙利用了BaseDexClassLoader的机制,类似于dex分包技术。
- Proxy/Delegate的方案则是使用ProxyApplication动态加载主程序dex。
###Xposed优缺点
优点
1)无需重启就可以达到修复bug的目的
2)多个模块可同时进行安装
3)无需修改任何的APK
缺点
1)需要Android 的root权限
2)Dexposed不支持Art模式(5.0+),且写补丁有点困难,需要反射写混淆后的代码,粒度太细,要替换的方法多的话,工作量会比较大。
###AndFix
①原理:AndFix的原理就是方法的替换,把有bug的方法替换成补丁文件中的方法。
注:在Native层使用指针替换的方式替换bug方法,以达到修复bug的目的。
加载补丁文件
获取补丁方法
获取bug方法
jni层替换方法
修复补丁的具体过程是:
1)我们及时修复好bug之后,我们可以apkpatch工具将两个apk做一次对比,然后找出不同的部分。可以看到生成的apatch了文件。若果这个时候,我们把后缀改成zip再解压开,里面有一个dex文件。反编译之后查看一下源码,里面就是被修复的代码所在的类文件,这些更改过的类都加上了一个_CF的后缀,并且变动的方法都被加上了一个叫@MethodReplace的annotation,通过clazz和method指定了需要替换的方法。
2)客户端得到补丁文件后就会根据annotation来寻找需要替换的方法。从AndFixManager.fix方法开始,客户端找到对应的需要替换的方法,然后在fix方法的具体实现中调用fixClass方法进行方法替换过程。
3)由JNI层完成方法的替换。fixClass方法遍历补丁class里的方法,在jni层对所需要替换的方法进行一一替换。
private void replaceMethod(ClassLoader classLoader, String clz, String meth, Method method) {
try {
String key = clz + "@" + classLoader.toString();
Class<?> clazz = mFixedClass.get(key);
if (clazz == null) {// class not load
// 要被替换的class
Class<?> clzz = classLoader.loadClass(clz);
// 这里也很黑科技,通过C层,改写accessFlags,把需要替换的类的所有方法(Field)改成了public,具体可以看Method结构体
clazz = AndFix.initTargetClass(clzz);
}
if (clazz != null) {// initialize class OK
mFixedClass.put(key, clazz);
// 需要被替换的函数
Method src = clazz.getDeclaredMethod(meth, method.getParameterTypes());
// 这里是调用了jni,art和dalvik分别执行不同的替换逻辑,在cpp进行实现
AndFix.addReplaceMethod(src, method);
}
} catch (Exception e) {
Log.e(TAG, "replaceMethod", e);
}
}
优点
1)可以多次打补丁。如果本地保存了多个补丁,那么AndFix会按照补丁生成的时间顺序加载补丁。具体是根据.apatch文件中的PATCH.MF的字段Created-Time。
2)安全性
readme提示开发者需要验证下载过来的apatch文件的签名是否就是在使用apkpatch工具时使用的签名,如果不验证那么任何人都可以制作自己的apatch文件来对你的APP进行修改。 但是我看到AndFix已经做了验证,如果补丁文件的证书和当前apk的证书不是同一个的话,就不能加载补丁。 官网还有一条,提示需要验证optimize file的指纹,应该是为了防止有人替换掉本地保存的补丁文件,所以要验证MD5码,然而SecurityChecker类里面也已经做了这个工作。。但是这个MD5码是保存在sharedpreference里面,如果手机已经root那么还是可以被访问的。
3)不需要重启APP即可应用补丁。
缺点
1)不支持YunOS 2)无法添加新类和新的字段 3)需要使用加固前的apk制作补丁,但是补丁文件很容易被反编译,也就是修改过的类源码容易泄露 4)使用加固平台可能会使热补丁功能失效 5)无法添加类和字段
###ClassLoader
大致的流程是:在dx工具执行之前,将LoadBugClass.class文件呢,进行修改,再其构造中添加System.out.println(dodola.hackdex.AntilazyLoad.class),然后继续打包的流程。注意:AntilazyLoad.class这个类是独立在hack.dex中。
总结下,其实我们需要做的就是两件事:
1、动态改变BaseDexClassLoader对象间接引用的dexElements;
2、在app打包的时候,阻止相关类去打上CLASS_ISPREVERIFIED标志。所有与该类相关的类都需要进行动态注入,可进行修改,也是需要进行fix的对象。
…
参考博客
###各个热修复方案优缺点
1、Dexpost:
1)原理:在底层虚拟机运行时hoop方法;
2)缺点:适配方面存在一些问题,目前不支持android6.0,5,1;art运行时;
3)优点:无需重启就可以达到修复bug的目的;
2、AndFix:
1)原理:在Native层使用指针替换的方法替换bug方法,达到修复bug的目的;
2)缺点:
底层替换,稳定性方面可能需要实际检测;
不支持YunOS;
无法添加新类和新的字段;
需要使用加固前的apk制作补丁,但是补丁文件很容易被反编译,也就是修改过的类源码容易泄露;
使用加固平台可能会使热补丁功能失效;
无法添加类和字段;
3)优点:无需中期就可以达到修复bug的目的;
3、HotFix:
1)原理:通过替换类加载器中bugclass,达到修复bug的目的;
2)缺点:需要重新启动才可以修复bug;
3)优点:java运行层修复,稳定性较好;
4、Nuwa:
1)原理:通过替换类加载器中bugclass,达到修复bug的目的;
2)缺点:需要重新启动才可以修复bug;
3)优点:java运行层修复,稳定性较好;自动化热修复
5、DroidFix:
1)原理:通过替换类加载器中bugclass,达到修复bug的目的;
2)缺点:需要重新启动才可以修复bug;
3)优点:java运行层修复,稳定性较好;
6、dynamic-load-apk
1)缺点:不支持Service和BroadcastReceiver;迁移成本高,需要修改插件,插件app需要继承自proxyActivity
2)优点: 插件无需安装host即可吊起;支持R访问插件资源;插件支持Activity和FragmentActivity;基本无反射调用;插件安装后任可独立运行
7、Droid Plugin
1)缺点:无法使用自定义资源的通知;法注册一些特殊Intent Filter的组件(四大组件);对Native支持不好
2)优点:插件无需任何修改,可独立安装运行,也可以做插件运行;四大组件无需在Host程序注册;超强隔离性,不同插件运行在不同的进程中;资源完全隔离;实现进程管理,插件的空进程会被及时回收,占用内存低;插件的静态广播会被当作动态处理,如果插件没有运行,静态广播永远不会触发;API侵入性低
8、DynamicAPK
1)优点: 迁移成本低(无需做任何activity/fragment/resource的proxy实现)不使用代理来管理插件的activity/fragment的生命周期。修改后aapt会处理插件种的资源,R.java中的资源引用和普通Android工程没有区别,开发者可以保持原有的开发规范;
更加有利于并发开发;
提升编译速度;
提升启动速度。
2)缺点:
dex解压、dexopt、加载耗时较长,使用按需加载启动时间过长
###使用Tinker
请见我另一篇详细的tinker接入说明