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

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

相关文章

Android热修复学习之旅——Andfix框架完全解析

Android热修复学习之旅开篇——热修复概述 Android热修复学习之旅——HotFix完全解析 Android热修复学习之旅——Tinker接入全攻略 在之前的博客《Android热修复学习之旅——HotFix完全解析》中,我们学习了热修复的实现方式之一,通过dex分包方案的原理还有HotFix框架的源码分析,本次我将讲解热修复的另外一种思路,那就是通过native方法,使用这种思路

Android热修复学习之旅——HotFix完全解析

在上一篇博客 Android热修复学习之旅开篇——热修复概述中,简单介绍了各个热修复框架的原理,本篇博客我将详细分析QQ空间热修复方案。 Android dex分包原理介绍 QQ空间热修复方案基于Android dex分包基础之上,简单概述android dex分包的原理就是:就是把多个dex文件塞入到app的classloader之中,但是android dex拆包方案中的类是没有重复的,如

Android热修复学习之旅开篇——热修复概述

Android热修复技术无疑是Android领域近年来最火热的技术之一,同时也涌现了各种层出不穷的实现方案,如QQ空间补丁方案、阿里AndFix以及微信Tinker等等,从本篇博客开始,计划写一个系列博客专门介绍热修复的相关内容,本系列博客将一一介绍这些框架的原理和源码分析,作为本系列的开篇,本篇博客将对热修复技术进行一个概述,并对以上几种方案进行对比。 为什么会出现热修复? 简单来说,以前出

.hmallox勒索病毒解密方法|勒索病毒解决|勒索病毒恢复|数据库修复

导言: 在当今数字化时代,勒索病毒已经成为网络安全的一大威胁,其中包括了最近出现的.hmallox勒索病毒。这类恶意软件不仅能够对计算机系统进行加密,还会要求用户支付赎金以换取解密密钥,给个人用户和企业带来了严重的损失和困扰。当面对被勒索病毒攻击导致的数据文件加密问题时,您可添加我们的技术服务号(sjhf91)。我们将为您提供专业、快速的数据恢复技术支持。 攻击者的动机和策略 攻

修复漏洞Windows 2012 Server R2(CVE-2016-2183)、(CVE-2015-2808)、(CVE-2013-2566)

修复漏洞 漏洞风险等级评定标准主机风险等级评定标准漏洞概括利用注册表修复漏洞查看修复后的漏洞 漏洞风险等级评定标准 危险程度危险值区域危险程度说明高7 <=漏洞风险值<= 10攻击者可以远程执行任意命令或者代码,或对系统进行远程拒绝服务攻击。中4 <=漏洞风险值< 7攻击者可以远程创建、修改、删除文件或数据,或对普通服务进行拒绝服务攻击。低0 <=漏洞风险值< 4攻击者可以获取

Linux mint18的Mint-Y主题被改为xfce4之后修复

效果图: 当屏幕出现了上面这幅图的时候,就已经不再是浅绿色的主题了,需要进行主题的回复安装。回复安装主题的命令:LC_ALL=C apt-cache depends mint-meta-xfce | grep ‘[ |] Depends:[^<]’ | cut -d: -f2 | tr -d ’ ‘|xargs sudo apt-get –reinstall install -y安装之后发现

ChatTTS增强版V3【已开源】,长文本修复,中英混读,导入音色,批量SRT、TXT

ChatTTS增强版V3来啦!本次更新增加支持导入SRT、导入音色等功能。结合上次大家反馈的问题,修复了长文本、中英混读等问题。 项目已开源(https://github.com/CCmahua/ChatTTS-Enhanced) 项目介绍 V3 ChatTTS增强版V3,长文本修复,中英混读,导入音色,批量SRT、TXT,代码开源_哔哩哔哩_bilibili V2 ChatTTS

荒野大镖客2启动找不到emp.dll的7个修复方法,轻松解决dll丢失的办法

一、emp.dll文件丢失的常见原因 安装或更新问题:在软件或游戏的安装过程中,可能由于安装程序未能正确复制文件到目标目录,或在更新过程中文件被意外覆盖或删除,导致emp.dll文件丢失。 安全软件误删:某些安全软件可能会误判emp.dll文件为恶意软件,并将其隔离或删除。 用户操作失误:用户在手动修改游戏文件或进行mod操作时,可能会不小心删除或替换emp.dll文件。 环境配置错误:如

cpu漏洞修复

关于CPU相关漏洞的修复处理_cpu漏洞如何修复 vulnerability spec store bypass: vulnera-CSDN博客

Linux 内核权限提升漏洞CVE-2024-1086三种修复方法

作者介绍:老苏,10余年DBA工作运维经验,擅长Oracle、MySQL、PG数据库运维(如安装迁移,性能优化、故障应急处理等)公众号:老苏畅谈运维欢迎关注本人公众号,更多精彩与您分享。 一、漏洞概述 漏洞成因: Netfilter是Linux内核中的一个数据包处理模块,它可以提供数据包的过滤、转发、地址转换NAT功能。 2024年3月28日,监测到 Linux kernel权限