百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

大厂面试热点:“热修复机制及常见的几个框架介绍”

zhezhongyun 2025-06-24 18:08 35 浏览

前言

现在线上的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 架构师之路还很漫长,与君共勉

相关推荐

Python入门学习记录之一:变量_python怎么用变量

写这个,主要是对自己学习python知识的一个总结,也是加深自己的印象。变量(英文:variable),也叫标识符。在python中,变量的命名规则有以下三点:>变量名只能包含字母、数字和下划线...

python变量命名规则——来自小白的总结

python是一个动态编译类编程语言,所以程序在运行前不需要如C语言的先行编译动作,因此也只有在程序运行过程中才能发现程序的问题。基于此,python的变量就有一定的命名规范。python作为当前热门...

Python入门学习教程:第 2 章 变量与数据类型

2.1什么是变量?在编程中,变量就像一个存放数据的容器,它可以存储各种信息,并且这些信息可以被读取和修改。想象一下,变量就如同我们生活中的盒子,你可以把东西放进去,也可以随时拿出来看看,甚至可以换成...

绘制学术论文中的“三线表”具体指导

在科研过程中,大家用到最多的可能就是“三线表”。“三线表”,一般主要由三条横线构成,当然在变量名栏里也可以拆分单元格,出现更多的线。更重要的是,“三线表”也是一种数据记录规范,以“三线表”形式记录的数...

Python基础语法知识--变量和数据类型

学习Python中的变量和数据类型至关重要,因为它们构成了Python编程的基石。以下是帮助您了解Python中的变量和数据类型的分步指南:1.变量:变量在Python中用于存储数据值。它们充...

一文搞懂 Python 中的所有标点符号

反引号`无任何作用。传说Python3中它被移除是因为和单引号字符'太相似。波浪号~(按位取反符号)~被称为取反或补码运算符。它放在我们想要取反的对象前面。如果放在一个整数n...

Python变量类型和运算符_python中变量的含义

别再被小名词坑哭了:Python新手常犯的那些隐蔽错误,我用同事的真实bug拆给你看我记得有一次和同事张姐一起追查一个看似随机崩溃的脚本,最后发现罪魁祸首竟然是她把变量命名成了list。说实话...

从零开始:深入剖析 Spring Boot3 中配置文件的加载顺序

在当今的互联网软件开发领域,SpringBoot无疑是最为热门和广泛应用的框架之一。它以其强大的功能、便捷的开发体验,极大地提升了开发效率,成为众多开发者构建Web应用程序的首选。而在Spr...

Python中下划线 ‘_’ 的用法,你知道几种

Python中下划线()是一个有特殊含义和用途的符号,它可以用来表示以下几种情况:1在解释器中,下划线(_)表示上一个表达式的值,可以用来进行快速计算或测试。例如:>>>2+...

解锁Shell编程:变量_shell $变量

引言:开启Shell编程大门Shell作为用户与Linux内核之间的桥梁,为我们提供了强大的命令行交互方式。它不仅能执行简单的文件操作、进程管理,还能通过编写脚本实现复杂的自动化任务。无论是...

一文学会Python的变量命名规则!_python的变量命名有哪些要求

目录1.变量的命名原则3.内置函数尽量不要做变量4.删除变量和垃圾回收机制5.结语1.变量的命名原则①由英文字母、_(下划线)、或中文开头②变量名称只能由英文字母、数字、下画线或中文字所组成。③英文字...

更可靠的Rust-语法篇-区分语句/表达式,略览if/loop/while/for

src/main.rs://函数定义fnadd(a:i32,b:i32)->i32{a+b//末尾表达式}fnmain(){leta:i3...

C++第五课:变量的命名规则_c++中变量的命名规则

变量的命名不是想怎么起就怎么起的,而是有一套固定的规则的。具体规则:1.名字要合法:变量名必须是由字母、数字或下划线组成。例如:a,a1,a_1。2.开头不能是数字。例如:可以a1,但不能起1a。3....

Rust编程-核心篇-不安全编程_rust安全性

Unsafe的必要性Rust的所有权系统和类型系统为我们提供了强大的安全保障,但在某些情况下,我们需要突破这些限制来:与C代码交互实现底层系统编程优化性能关键代码实现某些编译器无法验证的安全操作Rus...

探秘 Python 内存管理:背后的神奇机制

在编程的世界里,内存管理就如同幕后的精密操控者,确保程序的高效运行。Python作为一种广泛使用的编程语言,其内存管理机制既巧妙又复杂,为开发者们提供了便利的同时,也展现了强大的底层控制能力。一、P...