大厂面试热点:“热修复机制及常见的几个框架介绍”
zhezhongyun 2025-06-24 18:08 3 浏览
前言
现在线上的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异常排查难度更高;
- 由于无法增加变量与类等限制,无法做到功能发布级别;
5.2 阿里AndFix -- 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等
5.3 QQ空间--Dex插桩方案
原理:
- 原理是Hook了ClassLoader.pathList.dexElements[]。因为ClassLoader的findClass是通过遍历dexElements[]中的dex来寻找类的。当然为了支持4.x的机型,需要打包的时候进行插桩。
- 越靠前的Dex优先被系统使用,基于类级别的修复
优点:
- 不需要考虑对dalvik虚拟机和art虚拟机做适配
- 代码是非侵入式的,对apk体积影响不大
缺点:
- 需要下次启动才会生效
- 最大挑战在于性能,即Dalvik平台存在插桩导致的性能损耗,Art平台由于地址偏移问题导致补丁包可能过大的问题
- 虚拟机在安装期间为类打上CLASS_ISPREVERIFIED标志是为了提高性能的,我们强制防止类被打上标志是否会影响性能?这里我们会做一下更加详细的性能测试.但是在大项目中拆分dex的问题已经比较严重,很多类都没有被打上这个标志。
5.4 美团Robust -- Instant Run 热插拔
原理:
- 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内联的函数不能被内联了
5.5 微信Tinker
原理:
- 服务端做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本系统杀掉。
5.6 阿里Sophix
优化Andfix(突破底层结构差异,解决稳定性问题):
Andfix底层ArtMethod结构时采用内部变量一一替换,倒是这个各个厂商是会修改的,所以兼容性不好。
Sophix改变了一下思路,采用整体替换方法结构,忽略底层实现,从而解决兼容稳定性问题。
六、热修复需要解决的难点
热修复不同于插件化,不需要考虑各种组件的生命周期,唯一需要考虑的就是如何能将问题的方法/类/资源/so 替换为补丁中的新方法/类/资源/so,其中最重要的是方法和类的替换,所以有不少热修复框架只做了方法和类的替换,而没有对资源和 so 进行处理
热修复框架普遍存在一个问题: 虽然不用安装新版本的安装包同样可以修复bug,但是如果本地下载好的补丁包被删除了,那么之前bug就会重新!因为热修复不是合拼生成新的apk,而是 动态加载修复bug的那部分代码。换句话说修复bug的代码是存放在补丁包里的,删除补丁包,修复bug的代码也就不存在了.之前bug也就重新出来了。
总结
现在的热修补的技术可以说是百花齐放了,很多大型的公司都有自己完整的热修复技术框架,但是想要深入了解热修复,就需要先去了解其中的一些机制,很多机制需要庞大的知识贮备才能进行深入理解,当然Android Framwork的实现细节是非常重要的
热修复不是简单的客户端SDK,它还包含了安全机制和服务端的控制逻辑,整条链路也不是短时间可以快速完成的。所以需要我们深入了解才能更好的去理解。
有需要文中完整代码的同学 可以 私信发送 “底层源码” 即可 免费获取
现在私信还可以获得 更多《Android 学习笔记+源码解析+面试视频》
最后我想说:
对于程序员来说,要学习的知识内容、技术有太多太多,要想不被环境淘汰就只有不断提升自己,从来都是我们去适应环境,而不是环境来适应我们
技术是无止境的,你需要对自己提交的每一行代码、使用的每一个工具负责,不断挖掘其底层原理,才能使自己的技术升华到更高的层面
Android 架构师之路还很漫长,与君共勉
相关推荐
- DevExpress使用教程:GridView经验小结
-
下面是笔者自己总结的使用DevExpressGridview的一些经验小结,分享给大家:1、去除GridView头上的"Dragacolumnheaderheretogroup...
- ComponentOne 新版本发布,新增 .NET 6 和 Blazor 平台控件支持
-
ComponentOneEnterprise是葡萄城推出的一款内置300多种开发控件的.NET控件集,可满足WinForm、WPF、Blazor、ASP.NETMVC等平台下的系统开发...
- Wijmo5 Flexgrid基础教程:数据绑定
-
WijmoEnterprise下载>FlexGrid在JavaScript程序中启动添加Wijmo引用;添加wijmo控件的扩展;在JavaScript中初始化wijmo控件;(可选)添加cs...
- Wijmo5 Flexgrid基础教程:InlineEdit
-
WijmoEnterprise下载>对于flexgrid,可以直接在单元格内进行编辑。但另外还有一种编辑方式,即在一行添加按钮,统一的编辑和提交数据。本文主要介绍给flexgrid添加编辑按钮...
- WinForms Data Grid控件升级(winform devexpress控件)
-
告诉大家一个好消息:慧都将于近期隆重推出“DevExpress14.2新版发布会”。心动不如行动,赶快报名吧!我们期待与您相约DevExpress14.2新版发布会。>>新增Wind...
- XAML控件宽度为另一控件的一半、静态属性绑定
-
控件上当某些数据需要根据其他数据的变化而变化很多时候,想让某个控件的宽度或者高度是另一个已有控件的一半,一开始打算使用ObjectDataProvider来实现,因为在控件上当某些数据需要根据其他数据...
- 用 CSS Grid 布局制作一个响应式柱状图
-
最新一段时间比较喜欢玩弄图表,出于好奇,我想找出比较好的用CSS制作图表的方案。开始学习网上开源图表库,它对我学习新的和不熟悉的前端技术很有帮助,比如这个:CSSGrid。今天和大家分享我学到的...
- Grid 移动端双列瀑布流(移动端瀑布流布局)
-
预览图:原理合理使用Grid的属性:display:设置为grid指明当前容器为Grid布局grid-template-columns:定义每一列的列宽(百分比或绝对单位)grid-templa...
- DevExpress导出GridControl控件数据
-
前言:使用C#做桌面应用时,我们会常常使用Winform作为我们的开发界面,但是windows自带的控件由于长时间不更新,已经不能够满足当前开发需要所以使用DevExpress控件作为Winform...
- css grid 布局的那些事儿(css grid布局和flex布局)
-
CSSGrid是一种为Web开发创建网站布局的方式。它已经存在了很多年,随着更多浏览器的支持,它终于变得越来越流行。接下来我们将了解下CSSGrid及其工作原理。了解下它如何使用。CSS...
- Grid.js - 跨框架的前端表格插件(前端table框架)
-
只想简简单单画个表格,但React,Vue,Angular,…,这么多前端框架,各自都有不同的表格渲染库。就没有表格库能“一次画表,到处运行”吗?来看看Grid.js这个跨框架的前端表格插件吧!...
- WPF开发教程01-布局控件(wpf tablecontrol控件)
-
布局控件是用于进行控件布局的容器类控件,其内部控件按照一定规律自动排列,且在父控件改变大小时,会自动适应。常用布局控件如下:1.一维布局控件(StackPanel)其内部控件按照某个维度自动排列,排...
- wxPython - 高级控件之表格Grid(wxpython grid刷新数据)
-
实战wxPython系列-043wx.grid.Grid及其相关类用于显示和编辑表格数据。它们提供了一组丰富的功能,用于显示、编辑和与各种数据源交互。wx.grid.Grid是一个功能强大的但是又稍微...
- 前端 BFC、IFC、GFC 和 FFC,这些你都知道吗?
-
如果觉得我的文章不错,可以关注我,想要看其他的进阶知识可以查看我发布过的文章!编辑搜图请点击输入图片描述BFC(Blockformattingcontexts):块级格式上下文页面上的一个隔离的...
- 20多个好用的 Vue 组件库,请查收
-
在本文中,我们将探讨一些最常见的vuejs组件。你可以收藏一波。VueTables-2地址:https://github.com/matfish2/vue-tables-2VueTables2...
- 一周热门
- 最近发表
-
- DevExpress使用教程:GridView经验小结
- ComponentOne 新版本发布,新增 .NET 6 和 Blazor 平台控件支持
- Wijmo5 Flexgrid基础教程:数据绑定
- Wijmo5 Flexgrid基础教程:InlineEdit
- WinForms Data Grid控件升级(winform devexpress控件)
- XAML控件宽度为另一控件的一半、静态属性绑定
- 用 CSS Grid 布局制作一个响应式柱状图
- Grid 移动端双列瀑布流(移动端瀑布流布局)
- DevExpress导出GridControl控件数据
- css grid 布局的那些事儿(css grid布局和flex布局)
- 标签列表
-
- HTML 教程 (33)
- HTML 简介 (35)
- HTML 实例/测验 (32)
- HTML 测验 (32)
- JavaScript 和 HTML DOM 参考手册 (32)
- HTML 拓展阅读 (30)
- HTML常用标签 (29)
- HTML文本框样式 (31)
- HTML滚动条样式 (34)
- HTML5 浏览器支持 (33)
- HTML5 新元素 (33)
- HTML5 WebSocket (30)
- HTML5 代码规范 (32)
- HTML5 标签 (717)
- HTML5 标签 (已废弃) (75)
- HTML5电子书 (32)
- HTML5开发工具 (34)
- HTML5小游戏源码 (34)
- HTML5模板下载 (30)
- HTTP 状态消息 (33)
- HTTP 方法:GET 对比 POST (33)
- 键盘快捷键 (35)
- 标签 (226)
- HTML button formtarget 属性 (30)
- CSS 水平对齐 (Horizontal Align) (30)