热更新原理

article/2025/8/27 8:21:47
对于热更新的问题就是了解两个点的问题:
  • 如何加载补丁包,也就是如何加载dex 文件的过程(dex是补丁包,更改的文件都在补丁包中)
  • 修复后的类如何替换掉旧的类

通过这篇文章给大家介绍下我理解的热更新的逻辑,需要先了解一些关系到的知识

热更新方案有三种

  • 底层替换方案
  • 类加载方案
  • Instant Run

本篇文章主要是 类加载 和 Instant Run 两种方式进行的热更新

类加载方案

需要先了解Android 类加载,可以看这篇 https://blog.csdn.net/hjiangshujing/article/details/104249956
此处用到的是Android 中的 DexClassLoader 类加载器
以下做简单的介绍

Android 类加载

  • BootClassLoader
  • DexClassLoader – optimizedDirect
  • PathClassLoader – 没有 optimizedDirect,默认optimizedDirect 的值为/data/dalvik-cache ,PathClassLoader 无法定义解压的dex文件存储路径,因此它通常用来加载已经安装的apk的dex文件(安装的apk的dex文件会存储在/data/dalvik-cache中)

类加载方案原理

首先要了解类加载过程

class 加载过程从ClassLoader 的loadClass方法开始
ClassLoader 的加载方法为loadClass

可以通过 Android 中的类加载 文章中的最后的图来说

在这里插入图片描述

DexPathList.java 中的findClass 方法(核心环节)
public Class<?> findClass(String name, List<Throwable> suppressed) {for (Element element : dexElements) {//1Class<?> clazz = element.findClass(name, definingContext, suppressed);//2if (clazz != null) {return clazz;}}if (dexElementsSuppressedExceptions != null) {suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));}return null;}
  • 遍历dexElements
  • 调用Element 的findClass 方法
  • 每个Element 中内部封装了DexFile ,用于加载dex,如果DexFile 不为null,就调用DexFile 的loadClassBinaryName 方法
  • loadClassBinaryName 方法中调用了defineClass,调用了defineClassNative方法来加载dex相关文件
那么类加载方案的原理就出来,如下:
  • ClassLoader 的加载过程,其中一个环节就是调用DexPathList的findClass 方法
  • Element 内部封装了DexFile ,DexFile用于加载dex文件,因此每个dex文件对应一个Element ,多个Element 组成了有序的Element 数组dexElements。
  • 当要查找类时,会遍历dexElements (相当于遍历dex 文件数组),并调用DexFile的loadClassBinaryName 方法查找类
  • 如果在Element 中(dex 文件)找到了该类就返回
  • 如果没有找到就接着在下一个Elment中进行查找
  • 将Bug类Key.class进行修改,再将Key.class打包成包含dex的补丁包Patch.jar
  • 通过反射修改类加载中的dexElements,将补丁包放在Elment 数组dexElements 的第一个元素
  • 在类加载的时候先加载到的是Patch.dex中的修复后的Key.class(根据类加载的双亲委托机制,如果此类加载过就不会再加载),这样就做到替换之前存在Bug的Key.class
代码实现
	/*** 修复方法*/private static void dexFix(ClassLoader classLoader, File optimizedDirectory, File... dexFiles) throws NoSuchFieldException, NoSuchMethodException, IllegalAccessError, IllegalAccessException {StringBuilder sb = new StringBuilder();for (File file : dexFiles) {sb.append(file.getAbsolutePath()).append(":");}//1.使用DexClassLoader 加载所有外部dex文件DexClassLoader dexClassLoader = new DexClassLoader(sb.deleteCharAt(sb.length() - 1).toString(),optimizedDirectory.getAbsolutePath(), null, ClassLoader.getSystemClassLoader());//2.获取系统 dexElementsObject pathElements = getClassLoaderElements(classLoader);//3.获取外部dex 的dexElements  Object dexElements = getClassLoaderElements(dexClassLoader);Field pathListField = ReflectUtil.findFiled(classLoader.getClass(), "pathList");Object pathList = pathListField.get(classLoader);Field dexElementsFiled = ReflectUtil.findFiled(pathList.getClass(), "dexElements");//4.将系统与外部dexElements合并Object arrayAppend = arrayAppend(dexElements, pathElements);//5.修改系统 dexElementsdexElementsFiled.set(pathList, arrayAppend);}/*** 将所有Array类型的数据按顺序合并成一个Array数据*/public static Object arrayAppend(Object... elements) {int length = 0;for (Object element : elements) {length += Array.getLength(element);}Object array = Array.newInstance(elements[0].getClass().getComponentType(), length);for (int i = 0, j = 0, k = 0, elementLength = Array.getLength(elements[k]); i < length; i++) {Array.set(array, i, Array.get(elements[k], i - j));if (i - j == elementLength - 1) {j += elementLength;k++;if (k < elements.length) {elementLength = Array.getLength(elements[k]);}}}return array;}/*** 获取ClassLoader 中 dexElements 成员变量*/private static Object getClassLoaderElements(ClassLoader classLoader) throws NoSuchMethodException, IllegalAccessException {Field pathListField = ReflectUtil.findFiled(classLoader.getClass(), "pathList");Object pathList = pathListField.get(classLoader);Field dexElementsFiled = ReflectUtil.findFiled(pathList.getClass(), "dexElements");return dexElementsFiled.get(pathList);}
缺点:
  • 类加载方案需要重启App 后ClassLoader重新加载新类
  • 因为类无法被卸载,想要重新加载新的类就需要重启App
  • 因此采用类加载方案的热修复框架不能即时生效
  • 如:QQ空间的超级补丁 ,Nuwa ,微信的Tinker ,饿了么Amigo 都是使用通过操作Element 数组实现的

Instant Run (立即更新)

什么是Instant Run

Instant Run 是Android Studio 2.0 以后新增的一个运行机制,能够显著减少开发人员第二次以及以后的构建和部署时间

使用Instant Run 前后编译部署应用程序流程的区别
  • 之前:代码更改 -> 构建和部署app -> app销毁->app 重启 -> activity重启->更改的代码运行
  • 之后:代码更改->构建改变部分->部署更改部分->(Cold Swap<App 重启>, Hot Swap ,Warm Swap<Activity 重启>)->更改的代码运行
  • 传统的编译部署需要重新安装App和重启App,会比较耗时
    Instant Run 的构建和部署都是基于更改的部分
Instant Run 原理

Instant Run原理就是:Instant Run 在第一次构建 APK 时,使用 ASM 在每一个方法中注入了判断代码

ASM :是一个 Java 字节码操控框架,它能够动态生成类或者增强现有类的功能。 ASM 可以直接产生 clsss文件,也可以在类被加载到虚拟机之前动态改变类的行为。

通过Instant Run进行热更新的步骤
  • 通过一些工具在类被加载到虚拟机之前动态的在类中每个方法上注入判断代码
  • 注入的代码内容大致为:是否需要加载补丁
  • 如果需要加载补丁,使用DexClassLoader类加载器加载指定地址的修复包,然后用DexClassLoader 的 loadClass方法加载补丁类,再通过反射使用这个被加载的补丁类
怎么使用Instant Run进行动态更新的 (以美团热更新Robust 为例简单列举下)
实现原理可以围绕着两点
  1. 代码注入
  2. 替换老的逻辑(加载补丁包)

Robust 中的几个重要类介绍:

PatchesInfo 接口 (用于保存修复类和旧类的类信息)
  • 补丁包说明类,可以获取所有补丁对象,每个对象包含被修复类名及该类对应的补丁类
  • 每个修复包中必须有一个类需要实现这个,用于存放此修复包中所有需要修复的类信息
  • 通过这个接口获取到指定修复的类和旧类信息
PatchedClassInfo
  • private String patchedClassName;//需要修复的类名称
  • private String patchClassName;//patch中的补丁类名称
  • 存放已修复类和旧类的类名,用于后续的动态加载
ChangeQuickRedirect 接口
  • 每个补丁类必须实现ChangeQuickRedirect接口,内部有两个方法
  • isSupport 判断当前方法是否执行补丁逻辑
  • accessDispatch具体修复逻辑
PatchProxy

此类是对ChangeQuickRedirect修复类做了一层包装,最终还是调用的ChangeQuickRedirect实现类中的方法

实现 ChangeQuickRedirect 的类

每个补丁类必须实现的接口(ChangeQuickRedirect),内部有两个方法

两个方法
  • isSupport 判断当前方法是否执行补丁逻辑
  • accessDispatch具体修复逻辑
方法参数
  • 第一个参数是方法的签名,这个签名的格式很简单:方法所属类全称:方法名:方法是否为static类型,注意中间使用冒号进行连接的。
  • 第二个参数是方法的参数信息,而对于这个参数后面分析动态插入代码逻辑的时候会发现操作非常麻烦,才把这个参数弄到手的。
Robust 的实现为:
  • 可以看到Robust为每个class增加了一个类型为ChangeQuickRedirect的静态成员,而在每个方法前都插入了使用changeQuickRedirect相关的逻辑,
  • 当changeQuickRedirect不为null时,可能会执行到accessDispatch从而替换掉之前老的逻辑
代码注入

原方法

public long getIndex() {  return 100;  
} 

将方法中注入判断代码后

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;  
}
加载补丁包
重点方法在PatchExecutor 中的patch 方法
public class PatchExecutor extends Thread {protected Context context;protected PatchManipulate patchManipulate;protected RobustCallBack robustCallBack;public PatchExecutor(Context context, PatchManipulate patchManipulate, RobustCallBack robustCallBack) {this.context = context.getApplicationContext();this.patchManipulate = patchManipulate;this.robustCallBack = robustCallBack;}@Overridepublic void run() {try {//拉取补丁列表List<Patch> patches = fetchPatchList();//应用补丁列表applyPatchList(patches);} catch (Throwable t) {Log.e("robust", "PatchExecutor run", t);robustCallBack.exceptionNotify(t, "class:PatchExecutor,method:run,line:36");}}/*** 拉取补丁列表*/protected List<Patch> fetchPatchList() {return patchManipulate.fetchPatchList(context);}/*** 应用补丁列表*/protected void applyPatchList(List<Patch> patches) {if (null == patches || patches.isEmpty()) {return;}Log.d("robust", " patchManipulate list size is " + patches.size());for (Patch p : patches) {if (p.isAppliedSuccess()) {Log.d("robust", "p.isAppliedSuccess() skip " + p.getLocalPath());continue;}if (patchManipulate.ensurePatchExist(p)) {boolean currentPatchResult = false;try {currentPatchResult = patch(context, p);} catch (Throwable t) {robustCallBack.exceptionNotify(t, "class:PatchExecutor method:applyPatchList line:69");}if (currentPatchResult) {//设置patch 状态为成功p.setAppliedSuccess(true);//统计PATCH成功率 PATCH成功robustCallBack.onPatchApplied(true, p);} else {//统计PATCH成功率 PATCH失败robustCallBack.onPatchApplied(false, p);}Log.d("robust", "patch LocalPath:" + p.getLocalPath() + ",apply result " + currentPatchResult);}}}protected boolean patch(Context context, Patch patch) {if (!patchManipulate.verifyPatch(context, patch)) {robustCallBack.logNotify("verifyPatch failure, patch info:" + "id = " + patch.getName() + ",md5 = " + patch.getMd5(), "class:PatchExecutor method:patch line:107");return false;}ClassLoader classLoader = null;try {File dexOutputDir = getPatchCacheDirPath(context, patch.getName() + patch.getMd5());classLoader = new DexClassLoader(patch.getTempPath(), dexOutputDir.getAbsolutePath(),null, PatchExecutor.class.getClassLoader());} catch (Throwable throwable) {throwable.printStackTrace();}if (null == classLoader) {return false;}Class patchClass, sourceClass;Class patchesInfoClass;PatchesInfo patchesInfo = null;try {Log.d("robust", "patch patch_info_name:" + patch.getPatchesInfoImplClassFullName());patchesInfoClass = classLoader.loadClass(patch.getPatchesInfoImplClassFullName());patchesInfo = (PatchesInfo) patchesInfoClass.newInstance();} catch (Throwable t) {Log.e("robust", "patch failed 188 ", t);}if (patchesInfo == null) {robustCallBack.logNotify("patchesInfo is null, patch info:" + "id = " + patch.getName() + ",md5 = " + patch.getMd5(), "class:PatchExecutor method:patch line:114");return false;}//classes need to patch 1.获取补丁包中所有待修复类信息List<PatchedClassInfo> patchedClasses = patchesInfo.getPatchedClassesInfo();if (null == patchedClasses || patchedClasses.isEmpty()) {
//            robustCallBack.logNotify("patchedClasses is null or empty, patch info:" + "id = " + patch.getName() + ",md5 = " + patch.getMd5(), "class:PatchExecutor method:patch line:122");//手写的补丁有时候会返回一个空listreturn true;}boolean isClassNotFoundException = false;for (PatchedClassInfo patchedClassInfo : patchedClasses) {String patchedClassName = patchedClassInfo.patchedClassName;String patchClassName = patchedClassInfo.patchClassName;if (TextUtils.isEmpty(patchedClassName) || TextUtils.isEmpty(patchClassName)) {robustCallBack.logNotify("patchedClasses or patchClassName is empty, patch info:" + "id = " + patch.getName() + ",md5 = " + patch.getMd5(), "class:PatchExecutor method:patch line:131");continue;}Log.d("robust", "current path:" + patchedClassName);try {try {//2.加载要被修复的类sourceClass = classLoader.loadClass(patchedClassName.trim());} catch (ClassNotFoundException e) {isClassNotFoundException = true;
//                    robustCallBack.exceptionNotify(e, "class:PatchExecutor method:patch line:258");continue;}Field[] fields = sourceClass.getDeclaredFields();Log.d("robust", "oldClass :" + sourceClass + "     fields " + fields.length);Field changeQuickRedirectField = null;for (Field field : fields) {if (TextUtils.equals(field.getType().getCanonicalName(), ChangeQuickRedirect.class.getCanonicalName()) && TextUtils.equals(field.getDeclaringClass().getCanonicalName(), sourceClass.getCanonicalName())) {//3.找到要被修复类的注入的静态变量changeQuickRedirectField = field;break;}}if (changeQuickRedirectField == null) {robustCallBack.logNotify("changeQuickRedirectField  is null, patch info:" + "id = " + patch.getName() + ",md5 = " + patch.getMd5(), "class:PatchExecutor method:patch line:147");Log.d("robust", "current path:" + patchedClassName + " something wrong !! can  not find:ChangeQuickRedirect in" + patchClassName);continue;}Log.d("robust", "current path:" + patchedClassName + " find:ChangeQuickRedirect " + patchClassName);try {//4.加载要修复类对应的patch中的补丁类对象patchClass = classLoader.loadClass(patchClassName);Object patchObject = patchClass.newInstance();changeQuickRedirectField.setAccessible(true);changeQuickRedirectField.set(null, patchObject);//5.将静态变量的值设置为补丁包中的补丁类//patchObject为补丁类对象Log.d("robust", "changeQuickRedirectField set success " + patchClassName);} catch (Throwable t) {Log.e("robust", "patch failed! ");robustCallBack.exceptionNotify(t, "class:PatchExecutor method:patch line:163");}} catch (Throwable t) {Log.e("robust", "patch failed! ");
//                robustCallBack.exceptionNotify(t, "class:PatchExecutor method:patch line:169");}}Log.d("robust", "patch finished ");if (isClassNotFoundException) {return false;}return true;}private static final String ROBUST_PATCH_CACHE_DIR = "patch_cache";/** @param c* @return 返回缓存补丁路径,一般是内部存储,补丁目录*/private static File getPatchCacheDirPath(Context c, String key) {File patchTempDir = c.getDir(ROBUST_PATCH_CACHE_DIR + key, Context.MODE_PRIVATE);if (!patchTempDir.exists()) {patchTempDir.mkdir();}return patchTempDir;}}
重点代码
  • 注解0处: 用DexClassLoader加载补丁包中的 PatchesInfoImpl 类,通过反射拿到这个 PatchesInfoImpl 实例对象。
  • 注释1处:通过PatchesInfoImpl获取补丁包中所有待修复类信息
  • 注释2处:用DexClassLoader加载要被修复的类
  • 注释3处:找到要被修复类的注入的静态变量
  • 注释4处:用DexClassLoader加载要修复类对应的patch中的补丁类对象
  • 注释5处:将静态变量的值设置为补丁包中的补丁类,代码中patchObject为补丁类对象
加载补丁包原理
  • 使用DexClassLoader加载指定地址的修复包
  • 然后用DexClassLoader 的 loadClass方法加载补丁类,new出新对象,
  • 在用反射把这新的补丁对象设置到旧类的changeQuickRedirect静态变量中即可。

http://chatgpt.dhexx.cn/article/pBfAAqpu.shtml

相关文章

Cocos Creator 3.x 热更新

前言&#xff1a;游戏做热更新 是基本需求&#xff1b; 好在 cocos-creator 已经为我们做好了方案&#xff0c;相对于 U3D 的热更新方案来说&#xff0c;使用起来很简便&#xff01;&#xff0c;不用关注很多细节 本文使用的是 cocos-creator 3.5.2 版本 官方文档 &#xff1…

热更新原理及实践注意

首先要说明几个概念&#xff0c;不要混用&#xff0c;热部署&#xff0c;热加载&#xff1b; 热部署&#xff1a;就是已经运行了项目,更改之后,不需要重新tomcat,但是会清空内存,重新打包,重新解压war包运行&#xff0c;可能好处是一个tomcat多个项目,不必因为tomcat停止而停止…

热更新你都知道哪些?

热更新系列目录 热更新你都知道哪些&#xff1f;热更新Sophix的爬坑之路腾讯热更新Tinker的故事阿里热更新Sophix的故事 Android热更新 前言1. 什么是热更新&#xff1f;2. 主流热更新方案3. 腾讯系热更新4. 阿里系热更新总结 博客创建时间&#xff1a;2020.05.16 博客更新时间…

热更新技术简易原理及技术推荐

为了照顾萌新童鞋&#xff0c;最开始还是对热更新的概念做一个通俗易懂的介绍。 热更新用通俗的讲就是软件不通过应用商店的软件版本更新审核&#xff0c;直接通过应用自行下载的软件数据更新的行为。在用户下载安装App之后&#xff0c;打开App时遇到的即时更新&#xff0c;是…

热更新及其原理

热更新&#xff1a;是app常用的更新方式&#xff0c;只需下载安装更新部分的代码 工作原理&#xff1a;动态下开发代码&#xff0c;使开发者在不发布新版本的情况下修复bug和发布功能&#xff0c;绕开苹果审核机制&#xff0c;避免长时间的审核以及多次被拒绝造成的成本。 优…

HTML/CSS实现小米官网搜索框效果

效果图&#xff1a; 需求分析&#xff1a; 1、输入框焦点事件 onfocus:成为焦点, 点击输入框的时候&#xff0c;出现闪烁光标&#xff0c;此时可以输入内容。 onblur :失去焦点, 点击页面空白区域&#xff0c;光标消失。此时不可以输入内容。 2、获取元素 3、注册事件 2.1…

html中的搜索代码,Web自动化(3):网页自动搜索功能

unsplash.jpg 写在前面 如果我们需要在期刊中搜索我们想要找的文章,那么我们如何才能达到这个目的。我们首先看一下,手动和自动对比图: 网页搜索.png 其实内容全部一样,我们只是用自动化程序,来代替我们手动操作。 1. 创建webdriver驱动对象,驱动打开网页 # 导入包 from …

java搜索代码_Java实现搜索功能代码详解

首先&#xff0c;我们要清楚搜索框中根据关键字进行条件搜索发送的是get请求&#xff0c;并且是向当前页面发送get请求 //示例代码 请求路径为当前页面路径 "/product" 当我们要实现多条件搜索功能时&#xff0c;可以将搜索条件封装为一个map集合&#xff0c;再根据m…

干货!最全优秀搜索框设计案例(含代码链接)

面对纷繁复杂的网页内容&#xff0c;用户通过查询关键词表达需求&#xff0c;期望在响应的查询结果中快速获取准确的信息和流畅的用户体验。用户与网络世界的万千联系都是从搜索开始的。搜索框之于用户就像是用户与应用或网站之间的对话窗口。小小的搜索框传递着用户与网站、应…

php网页制作中搜索框的代码,在网页里嵌入百度搜索框功能

今天发现某个网站是直接使用百度搜索作为自己网站的搜索功能的&#xff0c;感觉这个挺好玩的&#xff0c;不需要去研究复杂的搜索算法而又直接使用了百度搜索这个强大的搜索引擎为自己撑腰。无论对自己还是对用户来说都是相当不错的选择&#xff0c;下面作者将要和大家分享一下…

利用CSS写精美搜索框

利用html写搜索样式框 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>8款纯CSS3搜索框</title&g…

JavaScript实现智能搜索框

应用场景&#xff1a; 1. 搜索框在页面中占据的空间过小&#xff0c;希望无论浏览到什么位置&#xff0c;可以轻易地回到并聚焦搜索框。 2. 搜索框里面的文字大小过小&#xff0c;希望能够在上方开辟一块空间放大内容 解决思路&#xff1a; 1. 对整个页面添加键盘事件keyup…

html中搜索栏怎么写,html搜索框怎么做

很多:从记忆角度讲运用(谐音记忆法,联想法) 平常来说呢,可分为(死记硬背发,音标记忆法) 另外:自己创造 总结【谐音记忆法,死记硬背发,音标记忆法】 html/css如何写出如下搜索框效果&#xff0c;请给出代码 html制作一个搜索框&#xff0c;代码是什么&#xff1f; 打开Hbuilder编…

搜索导航HTML,CSS 带搜索导航栏的示例代码

本文为大家介绍如何使用 CSS 创建一个带搜索的导航栏。 以下实例均是响应式的。 可以先看下效果图: 创建一个搜索栏 主页 关于 联系我们 /* 在顶部导航栏中添加黑色背景颜色 */ .topnav {overflow: hidden; background-color: #e9e9e9; } /* 设置导航栏的链接样式 */ .topnav …

淘宝网搜索框源代码

搜索框代码&#xff1a; <script typetext/javascript> alimama_pidmm_11487878_0_0; alimama_typeg; alimama_tks{}; alimama_tks.style_i1; alimama_tks.lg_i1; alimama_tks.w_i350; alimama_tks.h_i69; alimama_tks.btn_i1; alimama_tks.txt_s; alimama_tks.hot_i1; a…

百度搜索框代码,实现回车点击,跳转到新页面,简单

参考网站&#xff1a;https://110.nanshiw.com/ 上图为实现效果 代码如下&#xff1a; <script> function butClick() {var val document.getElementById("search_key").value;if(val.length 0){alert(搜索为空&#xff0c;请输入内容);return false;}else…

智能搜索框html代码,js实现搜索框关键字智能匹配代码

只要使用搜索引擎的朋友应该都有这样的体会&#xff0c;就是当在搜索框输入关键字的时候&#xff0c;会出现自能匹配现象&#xff0c;这绝对是非常好的用户体验&#xff0c;下面就是一段类似的代码&#xff0c;当然这里只是掩饰&#xff0c;所以只能匹配的数据都是本地固定好的…

html5搜索框在最右侧,html5搜索框特效点击搜索框弹出分类搜索框代码

特效描述:html5搜索框特效 点击搜索框弹出 分类搜索框。点击搜索框弹出分类搜索框代码 代码结构 1. 引入CSS 2. 引入JS 3. HTML代码 Search People Sara Soueidan Rachel Smith Peter Finlan Patrick Cox

网页添加百度搜索框代码大全

★ 用法&#xff1a;在下面选择合适的样式&#xff0c;复制代码到网页中相应位置粘贴即可。 ★ 样式一&#xff08;20030&#xff09; 代码&#xff1a; <iframe id"baiduframe" marginwidth"0" marginheight"0" scrolling"no" fr…

【项目】实现网页搜索框功能

一、实现搜索框的部分代码 【注&#xff1a;涉及api接口和中后台数据交互】 1. 最终呈现形式&#xff1a; 2. 代码实现&#xff1a; HTML文件中&#xff1a; <!-- 搜索框部分 --><div class"search-bar" fxFlexAlign"center" style" ma…