热修复——紧急修复的大杀器

2024-04-05 01:04
文章标签 修复 紧急 杀器

本文主要是介绍热修复——紧急修复的大杀器,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

在实习中,我有幸参与了一项关键的任务,即实现应用程序的热修复功能。通过这个项目,我学习并了解了热修复技术,并且亲身体验了其在移动应用开发中的重要性和实际应用。

在本文中,我将分享我在实习期间学到的关于热修复的知识和经验。我会介绍热修复的概念、原理以及在 Android 应用开发中的实际应用。同时,我还会探讨一些常用的热修复框架,关注我,让我们一起讨论我在实践中遇到的一些挑战和解决方案。

通过本文的阅读,希望读者能够对热修复技术有一个全面的了解,并能够在自己的项目中灵活运用这一技术,提升应用程序的稳定性和可维护性。

一、热修复概念

在 Android 开发中,热修复(HotFix)是指在不重新发布整个应用程序的情况下,通过更新修复应用程序中的 bug 或者引入新功能。通常情况下,热修复技术允许应用程序在运行时动态地加载补丁(Patch),以解决应用程序的问题或者改进功能

于我而言,如果要给热修复一个大致的定义,那么简单来说,热修复所做的就是在不让用户有感知的情况下改变原有的Bug代码,已达成修复的目的。

二、热修复的适用场景

很多同学可能看完上面的概念以后,会产生一种疑问,有Bug解决不就好了,引入热修复干嘛?诶,您别说,你可就问对了。

当公司刚上线一批新的功能的时候,用户正在美滋滋的体验着App,虽然团队在发版前对新功能进行了大量测试,但是由于某种特殊原因,没有发现代码的漏洞,而应用的在线使用人群数量巨大,那么这个时候,你如果像正常那样去解决这个问题,那么你首先会经过产品介入,讨论如何解决这个问题;其次,需求对工作进行排期;然后去写技术方案,进行技术评审;再等技术评审通过后,再开始编码,编码完再进行测试,测试完再进行集成测试,最后再发版,发版后此时用户可以在用户商店看到自己的软件需要更新,当然软件自己也可以发出更新的通知,用户更新完,这个问题就解决了。

听我说完上面这段话,是不是觉得很麻烦?没错,确实麻烦,但是这是一个需求出现后的一个正常流程(当然每个公司都不一样)。如果每次都这样去解决问题,会消耗很多的人力物力,并且无法快速响应问题,造成用户的流失。而热修复,就不用像上面的新需求一样,去走那么繁琐的流程,还需要用户手动下载,一次两次可以接受,如果很多次呢?肯定会有部分的用户群体感到烦恼,那么热修复就可以完美的解决这个问题,在让用户零感知的情况下,偷偷的更新应用,将有问题的代码替换成修复后的代码,达成修复的目的,想想就香啊!

总的来说,热修复技术能够快速、高效地解决应用程序中的bug,提升用户体验,降低成本,增强应用程序的稳定性,是现代应用开发中不可或缺的一部分。

 

三、热修复的原理

前置知识

首先,我们需要知道,Android跟Java有很大的渊源,基于Jvm的Java应用是通过ClassLoader来加载应用中的class的,但我们知道Android对Jvm优化过,使用的是dalvik,且class文件会被打包进一个Dex文件中,底层虚拟机有所不同,那么它们的类加载器当然也是会有所区别,在Android中,要加载Dex文件中的class文件就需要用到 PathClassLoaderDexClassLoader 这两个Android专用的类加载器。

Dex是什么?

在明白什么是 Dex 文件之前,要先了解一下 JVM,Dalvik 和 ART。JVM 是 JAVA 虚拟机,用来运行 JAVA 字节码程序。Dalvik 是 Google 设计的用于 Android平台的运行时环境,适合移动环境下内存和处理器速度有限的系统。ART 即 Android Runtime,是 Google 为了替换 Dalvik 设计的新 Android 运行时环境,在Android 4.4推出。ART 比 Dalvik 的性能更好。Android 程序一般使用 Java 语言开发,但是 Dalvik 虚拟机并不支持直接执行 JAVA 字节码,所以会对编译生成的 .class 文件进行翻译、重构、解释、压缩等处理,这个处理过程是由 dx 进行处理,处理完成后生成的产物会以 .dex 结尾,称为 Dex 文件。Dex 文件格式是专为 Dalvik 设计的一种压缩格式。

所以可以简单的理解为:Dex 文件是很多 .class 文件处理后的产物,最终可以在 Android 运行时环境执行。

 Dex文件是怎么生成的?

可以很清晰的看到,是从.java->.class->.dex,这样的一个转换方式。

大杀器PathClassLoader DexClassLoader

在 Android 开发中,PathClassLoader DexClassLoader 是两个专门用于加载类文件的类加载器,它们在热修复中发挥着重要作用。简单来说,他们是热修复的两个大杀器!

PathClassLoader

  • 介绍:PathClassLoader 是 Android 系统提供的一个用于从已知路径加载类文件的类加载器。它主要用于加载应用程序 APK 文件中的类。
  • 特点:PathClassLoader 只能加载已安装在系统中的 APK 文件中的类,不能加载外部的未安装的 APK 文件。
  • 使用场景:主要用于加载应用程序自身的类文件,无法用于加载外部的热修复补丁。

DexClassLoader

  • 介绍:DexClassLoader 也是 Android 系统提供的一个类加载器,它可以从指定的路径加载包含 Dex 文件的 APK 或 Jar 文件,并生成对应的 DexClassLoader 实例。
  • 特点:DexClassLoader 可以加载外部的 Dex 文件,包括未安装的 APK 文件或者存储在设备上的独立 Dex 文件。
  • 使用场景:适用于热修复方案中,将修复后的 Dex 文件保存在设备上,然后通过 DexClassLoader 加载并实现热修复功能。

简单来说:

  • PathClassLoader:只能加载已经安装到Android系统中的apk文件(/data/app目录),是Android默认使用的类加载器。
  • DexClassLoader:可以加载任意目录下的dex/jar/apk/zip文件,比PathClassLoader更灵活,是实现热修复的重点

好了,前置知识已经讲的差不多了,接下来让我们进入正文,看看热修复是如何做到的。

热修复PathClassLoaderDexClassLoader源码

// PathClassLoader
public class PathClassLoader extends BaseDexClassLoader {public PathClassLoader(String dexPath, ClassLoader parent) {super(dexPath, null, null, parent);}public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {super(dexPath, null, librarySearchPath, parent);}
}// DexClassLoader
public class DexClassLoader extends BaseDexClassLoader {public DexClassLoader(String dexPath, String optimizedDirectory,String librarySearchPath, ClassLoader parent) {super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);}
}

 

通过比对,可以得出2个结论:

  • PathClassLoader与DexClassLoader都继承于BaseDexClassLoader
  • PathClassLoader与DexClassLoader在构造函数中都调用了父类的构造函数,但DexClassLoader多传了一个optimizedDirectory

BaseDexClassLoader

通过上面的源码,聪明的你肯定发现了,真正有意义的处理逻辑肯定在BaseDexClassLoader中,所以下面着重分析BaseDexClassLoader源码。

我们先来看看BaseDexClassLoader做了什么。(注意代码里面的DexPathList对象)

public class BaseDexClassLoader extends ClassLoader {...public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent){super(parent);this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);}...
}

 

  • dexPath:要加载的程序文件(一般是dex文件,也可以是jar/apk/zip文件)所在目录。
  • optimizedDirectory:dex文件的输出目录(因为在加载jar/apk/zip等压缩格式的程序文件时会解压出其中的dex文件,该目录就是专门用于存放这些被解压出来的dex文件的)。
  • libraryPath:加载程序文件时需要用到的库路径。
  • parent:父加载器

在 DexClassLoader 中,参数 optimizedDirectory 表示优化后的 Dex 文件存放目录。在加载 Dex 文件时,系统会将其中的 Dex 文件进行优化处理,然后将优化后的文件保存在指定的目录中,以提高加载速度和运行效率。

具体来说,optimizedDirectory 参数用于指定 Dex 文件的优化存放路径,加载器会将从 dexPath 指定的路径加载的 Dex 文件进行优化,并将优化后的文件保存在 optimizedDirectory 指定的目录中。这样,在后续加载该 Dex 文件时,系统会直接加载优化后的 Dex 文件,而不需要重新进行优化处理,从而提高加载速度。

在使用 DexClassLoader 进行热修复时,通常会将修复后的 Dex 文件保存在一个指定的目录中,并将该目录路径作为 optimizedDirectory 参数传入 DexClassLoader,以确保修复后的 Dex 文件能够被及时优化并加速加载。

 因为PathClassLoader只会加载已安装包中的dex文件,而DexClassLoader不仅仅可以加载dex文件,还可以加载jar、apk、zip文件中的dex,我们知道jar、apk、zip其实就是一些压缩格式,要拿到压缩包里面的dex文件就需要解压,所以,DexClassLoader在调用父类构造函数时会指定一个解压的目录。不过,从Android 8.0开始,BaseDexClassLoader的构造函数逻辑发生了变化,optimizedDirectory过时,不再生效,如果感兴趣的朋友可以去了解下,现在BaseDexClassLoader里面的逻辑是怎么样的。我个人认为,无论逻辑怎么变化,其实万变不离其宗~

目前来说我们还是没有看到它是怎么修复的,这个时候我们需要找到对应的class。我们的类加载器肯定会提供有一个方法来供外界找到它所加载到的class,该方法就是findClass(),不过在PathClassLoader和DexClassLoader源码中都没有重写父类的findClass()方法,但它们的父类BaseDexClassLoader就有重写findClass(),所以来看看BaseDexClassLoader的findClass()方法都做了哪些操作,代码如下:

private final DexPathList pathList;@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {List<Throwable> suppressedExceptions = new ArrayList<Throwable>();// 实质是通过pathList的对象findClass()方法来获取classClass 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;
}

 

可以看到,BaseDexClassLoader的findClass()方法实际上是通过DexPathList对象(pathList)的findClass()方法来获取class的,而这个DexPathList对象恰好在之前的BaseDexClassLoader构造函数中就已经被创建好了。所以,下面就来看看DexPathList类中都做了什么。

 我们核心介绍下以下两点

  • DexPathList的构造函数做了什么事?
  • DexPathList的findClass()方法是怎么获取class的?

构造函数

private final Element[] dexElements;public DexPathList(ClassLoader definingContext, String dexPath,String libraryPath, File optimizedDirectory) {...this.definingContext = definingContext;this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,suppressedExceptions);...
}

 

这个构造函数中,保存了当前的类加载器definingContext,并调用了makeDexElements()得到Element集合。

通过对splitDexPath(dexPath)的源码追溯,发现该方法的作用其实就是将dexPath目录下的所有程序文件转变成一个File集合。而且还发现,dexPath是一个用冒号(":")作为分隔符把多个程序文件目录拼接起来的字符串(如:/data/dexdir1:/data/dexdir2:...)。

那接下来无疑是分析makeDexElements()方法了,因为这部分代码比较长,我就贴出关键代码,并以注释的方式进行分析:

private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) {// 1.创建Element集合ArrayList<Element> elements = new ArrayList<Element>();// 2.遍历所有dex文件(也可能是jar、apk或zip文件)for (File file : files) {ZipFile zip = null;DexFile dex = null;String name = file.getName();...// 如果是dex文件if (name.endsWith(DEX_SUFFIX)) {dex = loadDexFile(file, optimizedDirectory);// 如果是apk、jar、zip文件(这部分在不同的Android版本中,处理方式有细微差别)} else {zip = file;dex = loadDexFile(file, optimizedDirectory);}...// 3.将dex文件或压缩文件包装成Element对象,并添加到Element集合中if ((zip != null) || (dex != null)) {elements.add(new Element(file, false, zip, dex));}}// 4.将Element集合转成Element数组返回return elements.toArray(new Element[elements.size()]);
}

 

在这个方法中,看到了一些眉目,总体来说,DexPathList的构造函数是将一个个的程序文件(可能是dex、apk、jar、zip)封装成一个个Element对象,最后添加到Element集合中。

其实,Android的类加载器(不管是PathClassLoader,还是DexClassLoader),它们最后只认dex文件,而loadDexFile()是加载dex文件的核心方法可以从jar、apk、zip中提取出dex。

再来看DexPathListfindClass()方法:

public Class findClass(String name, List<Throwable> suppressed) {for (Element element : dexElements) {// 遍历出一个dex文件DexFile dex = element.dexFile;if (dex != null) {// 在dex文件中查找类名与name相同的类Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);if (clazz != null) {return clazz;}}}if (dexElementsSuppressedExceptions != null) {suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));}return null;
}

结合DexPathList的构造函数,其实DexPathList的findClass()方法很简单,就只是对Element数组进行遍历,一旦找到类名与name相同的类时,就直接返回这个class,找不到则返回null。

为什么是调用DexFile的loadClassBinaryName()方法来加载class?这是因为一个Element对象对应一个dex文件,而一个dex文件则包含多个class。也就是说Element数组中存放的是一个个的dex文件,而不是class文件!!!这可以从Element这个类的源码和dex文件的内部结构看出。

 

 

经过对PathClassLoader、DexClassLoader、BaseDexClassLoader、DexPathList的分析,我们知道,安卓的类加载器在加载一个类时会先从自身DexPathList对象中的Element数组中获取(Element[] dexElements)到对应的类,之后再加载。采用的是数组遍历的方式,不过注意,遍历出来的是一个个的dex文件。

  在for循环中,首先遍历出来的是dex文件,然后再是从dex文件中获取class,所以,我们只要让修复好的class打包成一个dex文件,放于Element数组的第一个元素,这样就能保证获取到的class是最新修复好的class了(当然,有bug的class也是存在的,不过是放在了Element数组的最后一个元素中,所以没有机会被拿到而已)。

有人可能会问了,把Element数组放在原数组前面有什么用呢?还记得java的类加载机制吗?相同的类是不会加载第二次的!在 Java 的类加载机制中,对于同一个类,如果已经被加载过,那么就不会再次被加载,即使是在不同的类加载器中。因此,如果在同一个类加载器的作用域内,有多个 dex 文件或 jar 文件中含有相同的类,只会加载第一次遇到的那个类,后续的同名类不会再次加载。

总结:通过 PathClassLoader 和 DexClassLoader将需要修复的class打包成一个dex文件,然后放在Element数组的前面,实现bug代码的修复。

 

四、热修复的几种形式

本文的修复方式是主要针对代码的,还有些修复比如说一些资源的修复,动态链接库so的修复,下面我们来看看代码修复的几种方案。

代码层面的修复的几种方案:

1、类加载方案

(即本文主要讲解的)

 加载流程先是遵循双亲委派原则,如果委派原则没有找到此前加载过此类, 则会调用CLassLoader的findClass方法,再去BaseDexClassLoader下面的dexElements数组中查找,如果没有找到,最终调用defineClassNative方法加载

代码修复就是基于这点: 将新的做了修复的dex文件,通过反射注入到BaseDexClassLoader的dexElements数组的第一个位置上dexElements[0],下次重新启动应用加载类的时候,会优先加载做了修复的dex文件,这样就达到了修复代码的目的。

 

2、底层替换方案 

底层替换方案不会再次加载新类,而是直接在 Native 层 修改原有类, 这里我们需要提到Art虚拟机中ArtMethod: 每一个Java方法在Art虚拟机中都对应着一个 ArtMethodArtMethod记录了这个Java方法的所有信息,包括所属类、访问权限、代码执行地址等。有了地址,那么你只需要改变地址的指向就行了。虽然听着简单,但是这个过程中肯定会有很多奥秘和技术的,具体实现有兴趣的同学可以去了解下。

 3、插桩法

Instant Run 方案的核心思想是——插桩,在编译时通过插桩在每一个方法中插入代码,修改代码逻辑,在需要时绕过错误方法,调用patch类的正确方法。简而言之,即通过一个标记的形式来实现加载热修复的代码,虽然听着简单,但是这个过程中肯定会有很多奥秘和技术的,具体实现有兴趣的同学可以去了解下。

这篇关于热修复——紧急修复的大杀器的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/877279

相关文章

基于51单片机的自动转向修复系统的设计与实现

文章目录 前言资料获取设计介绍功能介绍设计清单具体实现截图参考文献设计获取 前言 💗博主介绍:✌全网粉丝10W+,CSDN特邀作者、博客专家、CSDN新星计划导师,一名热衷于单片机技术探索与分享的博主、专注于 精通51/STM32/MSP430/AVR等单片机设计 主要对象是咱们电子相关专业的大学生,希望您们都共创辉煌!✌💗 👇🏻 精彩专栏 推荐订阅👇🏻 单片机

【经验交流】修复系统事件查看器启动不能时出现的4201错误

方法1,取得『%SystemRoot%\LogFiles』文件夹和『%SystemRoot%\System32\wbem』文件夹的权限(包括这两个文件夹的所有子文件夹的权限),简单点说,就是使你当前的帐户拥有这两个文件夹以及它们的子文件夹的绝对控制权限。这是最简单的方法,不少老外说,这样一弄,倒是解决了问题。不过对我的系统,没用; 方法2,以不带网络的安全模式启动,运行命令行,输入“ne

六种msvcp110.dll丢失修复的方法分享,有效快速修复msvcp110.dll丢失

在日常使用电脑的过程中,我们可能会遭遇各种程序运行错误,其中“msvcp110.dll丢失”是一种非常常见的问题。这个问题通常发生在尝试启动某些程序时,系统会弹出一个错误消息,提示“程序无法启动,因为计算机缺少msvcp110.dll”,这可能会让用户感到困惑和无助。幸运的是,这个问题有多种解决方法,本文将指导你通过几种简单的步骤来修复“msvcp110.dll丢失”的问题,让你的程序回到正常运行

MyBatis-Plus 框架 QueryWrapper UpdateWrapper 方法修复sql注入漏洞事件

什么是漏洞? 漏洞是指软件、系统或网络中存在的安全弱点或错误,这些弱点可能导致系统遭受攻击或被不当使用。在计算机安全领域,漏洞通常源于编程错误、设计缺陷或配置失误。 对于对象关系映射(ORM)框架来说,漏洞通常指的是设计或实施中的安全问题,这些问题可能让应用程序面临SQL注入攻击的风险。 SQL 注入漏洞 如果ORM框架在执行SQL操作时没有正确过滤或转义用户输入,攻击者可以利用输入的恶意数据

《长得太长也是错?——后端 Long 型 ID 精度丢失的“奇妙”修复之旅》

引言 在前后端分离的时代,我们的生活充满了无数的机遇与挑战——包括那些突然冒出来的让人抓狂的 Bug。今天我们要聊的,就是一个让无数开发者哭笑不得的经典问题:后端 Long 类型 ID 过长导致前端精度丢失。说到这个问题,那可真是“万恶之源”啊,谁让 JavaScript 只能安全地处理 Number.MAX_SAFE_INTEGER(也就是 9007199254740991)以内的数值呢?

修复msvcp100.dll文件丢失的问题,如何高效率修复msvcp100.dll

在Windows操作系统中,msvcp100.dll是Microsoft Visual C++ 2010 Redistributable Package的一部分,它支持多种与C++库相关的关键功能。这个文件对于许多程序的正常运行非常重要。有时用户可能会遇到msvcp100.dll文件缺失的问题,这会导致某些程序无法启动或运行错误。本文将探讨一系列有效的解决方案,帮助用户修复msvcp100.dll

今天做了freemaker 导出word文档 的bug修复,解决 \n换行 问题

结合Freemaker导出文件 public void exportSimpleWord() throws Exception{// 要填充的数据, 注意map的key要和word中${xxx}的xxx一致Map<String,String> dataMap = new HashMap<String,String>();dataMap.put("username", "张三");dataMap.

前端 ESlint 代码规范及修复代码规范错误

代码规范及ESlint介绍 代码规范:一套写代码的约定规则。例如:赋值符号的左右是否需要空格?一句结束是否是要加分号? 虽然不遵守这些代码规范并不会造成语法错误,但是一个团队中,我们通常希望各个团队成员的代码风格是统一的 没有规矩不成方圆 ESLint:是一个代码检查工具,用来检查你的代码是否符合指定的规则(你和你的团队可以自行约定一套规则)。在创建项目时,我们使用的是 JavaSc

重要-准确-MySQL数据库主从修复

当从库数据需要重做时,可以用以下方法进行重建!! 主从数据库配置完整教程 环境设定: 主数据库:172.10.12.195 从数据库:172.10.12.200 数据库账号:root 数据库密码:xxxxxxx(为了安全,所有密码将用 xxxxxxx 代替) 1. 从主数据库导出数据 使用 mysqldump 工具从主数据库导出所有数据库: mysqldump -u root -p --

【软件合集】电脑桌面整理工具、DLL修复工具、文件加密等11款电脑必备软件,高效办公!

经常使用电脑办公的用户一定知道,第三方软件对于提高办公效率的影响力有多高! 除了电脑自带的功能之外,市面上还有很多好用的电脑软件,一款好用的电脑软件可以提高我们的办公效率,节省时间。 本期内容,小编整理了11款各种功能上的电脑软件,这些软件在各个领域都是有口碑的。常用的桌面整理软件、截图功能、系统重装、dll文件修复工具等,都能解决办公遇到的难题。 第1款、电脑桌面整理软件 应用场景