现在线上的BUG一直是令很多Android工程师所发愁的问题,可能就是那么几行代码,会让自己所研发的APP损失惨重,所以,热修复完美的解决了这些问题
下面就是我整理总结的一些热修复知识点和大厂热修复的一些相关资料
一、什么是热修复?热修复就是一个APP上线发布以后,发现自身存在很多BUG,想要修复这些BUG,但是如果重新推出一个版本、发布、再供用户下载,那样所用的时间就太久了,不利用户体验,所以热修复就出来了,他可以在用户所下载的APP里发布一个插件,他可以在不发布新版本的前提下,修复APP的BUG,这就叫热修复。
二、热修复的优势三、热修复机制
dexElements的数组
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="dart" cid="n259" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"> /**
* List of dex/resource (class path) elements.
* Should be called pathElements, but the Facebook app uses reflection
* to modify 'dexElements' (http://b/7726934).
*/
private final Element[] dexElements;</pre>
热修复就是利用dexElements的顺序来做文章,当一个补丁的patch.dex放到了dexElements的第一位,那么当加载一个bug类时,发现在patch.dex中,则直接加载这个类,原来的bug类可能就被覆盖了
看下PathClassLoader代码
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n262" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">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);
}
} </pre>
DexClassLoader代码
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="dart" cid="n264" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}</pre>
两个ClassLoader就两三行代码,只是调用了父类的构造函数.
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n266" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">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;
}</pre>
在BaseDexClassLoader 构造函数中创建一个DexPathList类的实例,这个DexPathList的构造函数会创建一个dexElements 数组
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="tsx" cid="n268" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) {
...
this.definingContext = definingContext;
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
//创建一个数组
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions);
...
}</pre>
然后BaseDexClassLoader 重写了findClass方法,调用了pathList.findClass,跳到DexPathList类中.
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="dart" cid="n270" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">/* package */final class DexPathList {
...
public Class findClass(String name, List<Throwable> suppressed) {
//遍历该数组
for (Element element : dexElements) {
//初始化DexFile
DexFile dex = element.dexFile;
if (dex != null) {
//调用DexFile类的loadClassBinaryName方法返回Class实例
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
return null;
}
...
} </pre>
会遍历这个数组,然后初始化DexFile,如果DexFile不为空那么调用DexFile类的loadClassBinaryName方法返回Class实例,归纳上面的话就是:ClassLoader会遍历这个数组,然后加载这个数组中的dex文件,而ClassLoader在加载到正确的类之后,就不会再去加载有Bug的那个类了,我们把这个正确的类放在Dex文件中,让这个Dex文件排在dexElements数组前面即可。
四、常见的几个热修复框架的对比热修复框架的种类繁多,按照公司团队划分主要有以下几种:
类别 |
成员 |
阿里系 |
AndFix、Dexposed、阿里百川、Sophix |
腾讯系 |
微信的Tinker、QQ空间的超级补丁、手机QQ的QFix |
知名公司 |
美团的Robust、饿了么的Amigo |
其他 |
RocooFix、Nuwa、AnoleFix |
虽然热修复框架很多,但热修复框架的核心技术主要有三类,分别是 代码修复、资源修复和动态链接库修复,其中每个核心技术又有很多不同的技术方案,每个技术方案又有不同的实现,另外这些热修复框架仍在不断的更新迭代中,可见热修复框架的技术实现是繁多可变的。
部分热修复框架的对比如下表所示:
特性 |
AndFix |
Tinker/Amigo |
QQ空间 |
Robust/Aceso |
即时生效 |
是 |
否 |
否 |
是 |
方法替换 |
是 |
是 |
是 |
是 |
类替换 |
否 |
是 |
是 |
否 |
类结构修改 |
否 |
是 |
否 |
否 |
资源替换 |
否 |
是 |
是 |
否 |
so替换 |
否 |
是 |
否 |
否 |
支持gradle |
否 |
是 |
否 |
否 |
支持ART |
是 |
是 |
是 |
是 |
支持Android7.0 |
是 |
是 |
是 |
是 |
我们可以根据上表和具体业务来选择合适的热修复框架,当然上表的信息很难做到完全准确,因为部分的热修复框架还在不断更新迭代。
五、技术原理及特点5.1 阿里Dexposed -- native原理:
- 直接在native层进行方法的结构体信息对换,从而实现完美的方法新旧替换,从而实现热修复功能
- 基于开源框架Xposed实现,是一种AOP解决方案
- 只Hook App本身的进程,不需要Root权限
优点:
- 即时生效
- 不需要任何编译器的插桩或者代码改写,对正常运行不引入任何性能开销。这是AspectJ之类的框架没法比拟的优势;
- 对所改写方法的性能开销也极低(微秒级),基本可以忽略不计;
- 从工程的角度来看,热补丁仅仅是牛刀小试,它真正的威力在于『线上调试』;
- 基于Xposed原理实现的AOP不仅可以hook自己的代码,还可以hook同进程的Android SDK代码,这也就可以让我们有能力在App中填上Google自己挖的坑。
缺点:
- Dalvik上近乎完美,不支持ART(需要另外的实现方式),所以5.0以上不能用了;
- 最大挑战在于稳定性与兼容性,而且native异常排查难度更高;
- 由于无法增加变量与类等限制,无法做到功能发布级别;
原理:
- 与Dexposed一样都基于开源框架Xposed实现,是一种AOP解决方案
优点:
- 即时生效
- 支持dalvik和art(AndFix supports Android version from 2.3 to 7.0, both ARM and X86 architecture, both Dalvik and ART runtime, both 32bit and 64bit.)
- 与Dexposed框架相比AndFix框架更加轻便好用,在进行热修复的过程中更加方便了
缺点:
- 面临稳定性与兼容性问题
- AndFix不支持新增方法,新增类,新增field等
原理:
- 原理是Hook了ClassLoader.pathList.dexElements[]。因为ClassLoader的findClass是通过遍历dexElements[]中的dex来寻找类的。当然为了支持4.x的机型,需要打包的时候进行插桩。
- 越靠前的Dex优先被系统使用,基于类级别的修复
优点:
- 不需要考虑对dalvik虚拟机和art虚拟机做适配
- 代码是非侵入式的,对apk体积影响不大
缺点:
- 需要下次启动才会生效
- 最大挑战在于性能,即Dalvik平台存在插桩导致的性能损耗,Art平台由于地址偏移问题导致补丁包可能过大的问题
- 虚拟机在安装期间为类打上CLASS_ISPREVERIFIED标志是为了提高性能的,我们强制防止类被打上标志是否会影响性能?这里我们会做一下更加详细的性能测试.但是在大项目中拆分dex的问题已经比较严重,很多类都没有被打上这个标志。
原理:
- Robust插件对每个产品代码的每个函数都在编译打包阶段自动的插入了一段代码,插入过程对业务开发是完全透明
- 编译打包阶段自动为每个class都增加了一个类型为ChangeQuickRedirect的静态成员,而在每个方法前都插入了使用changeQuickRedirect相关的逻辑,当 changeQuickRedirect不为null时,可能会执行到accessDispatch从而替换掉之前老的逻辑,达到fix的目的。
优点:
- 几乎不会影响性能(方法调用,冷启动)
- 支持Android2.3-8.x版本
- 高兼容性(Robust只是在正常的使用DexClassLoader)、高稳定性,修复成功率高达99.9%
- 补丁实时生效,不需要重新启动
- 支持方法级别的修复,包括静态方法
- 支持增加方法和类
- 支持ProGuard的混淆、内联、优化等操作
缺点:
- 代码是侵入式的,会在原有的类中加入相关代码
- so和资源的替换暂时不支持
- 会增大apk的体积,平均一个函数会比原来增加17.47个字节,10万个函数会增加1.67M。
- 会增加少量方法数,使用了Robust插件后,原来能被ProGuard内联的函数不能被内联了
原理:
- 服务端做dex差量,将差量包下发到客户端,在ART模式的机型上本地跟原apk中的classes.dex做merge,merge成为一个新的merge.dex后将merge.dex插入pathClassLoader的dexElement,原理类同Q-Zone,为了实现差量包的最小化,Tinker自研了DexDiff/DexMerge算法
Tinker还支持资源和So包的更新,So补丁包使用BsDiff来生成,资源补丁包直接使用文件md5对比来生成,针对资源比较大的(默认大于100KB属于大文件)会使用BsDiff来对文件生成差量补丁。
优点:
- 支持动态下发代码
- 支持替换So库以及资源
缺点:
- 不能即时生效,需要下次启动
Tinker已知问题:
- Tinker不支持修改AndroidManifest.xml,Tinker不支持新增四大组件(1.9.0支持新增非export的Activity);
- 由于Google Play的开发者条款限制,不建议在GP渠道动态更新代码;
- 在Android N上,补丁对应用启动时间有轻微的影响;
- 不支持部分三星android-21机型,加载补丁时会主动抛出"TinkerRuntimeException:checkDexInstall failed";
- 对于资源替换,不支持修改remoteView。例如transition动画,notification icon以及桌面图标。
Tinker性能痛点:
- Dex合并内存消耗在vm head上,容易OOM,最后导致合并失败。
- 如果本身app占用内存已经比较高,可能容易导致app本系统杀掉。
Andfix底层ArtMethod结构时采用内部变量一一替换,倒是这个各个厂商是会修改的,所以兼容性不好。
Sophix改变了一下思路,采用整体替换方法结构,忽略底层实现,从而解决兼容稳定性问题。
六、热修复需要解决的难点热修复不同于插件化,不需要考虑各种组件的生命周期,唯一需要考虑的就是如何能将问题的方法/类/资源/so 替换为补丁中的新方法/类/资源/so,其中最重要的是方法和类的替换,所以有不少热修复框架只做了方法和类的替换,而没有对资源和 so 进行处理
热修复框架普遍存在一个问题: 虽然不用安装新版本的安装包同样可以修复bug,但是如果本地下载好的补丁包被删除了,那么之前bug就会重新!因为热修复不是合拼生成新的apk,而是 动态加载修复bug的那部分代码。换句话说修复bug的代码是存放在补丁包里的,删除补丁包,修复bug的代码也就不存在了.之前bug也就重新出来了。
总结现在的热修补的技术可以说是百花齐放了,很多大型的公司都有自己完整的热修复技术框架,但是想要深入了解热修复,就需要先去了解其中的一些机制,很多机制需要庞大的知识贮备才能进行深入理解,当然Android Framwork的实现细节是非常重要的
热修复不是简单的客户端SDK,它还包含了安全机制和服务端的控制逻辑,整条链路也不是短时间可以快速完成的。所以需要我们深入了解才能更好的去理解。
有需要文中完整代码的同学 可以 私信发送 “底层源码” 即可 免费获取
现在私信还可以获得 更多《Android 学习笔记+源码解析+面试视频》
最后我想说:
对于程序员来说,要学习的知识内容、技术有太多太多,要想不被环境淘汰就只有不断提升自己,从来都是我们去适应环境,而不是环境来适应我们
技术是无止境的,你需要对自己提交的每一行代码、使用的每一个工具负责,不断挖掘其底层原理,才能使自己的技术升华到更高的层面
Android 架构师之路还很漫长,与君共勉
,