Android 热修复实现原理

 2023-09-05 阅读 71 评论 0

摘要:文章目录1、热修复背景2、Instant Run 概述3、类加载3、热修复3.1 代码修复3.1.1 类加载方案3.1.2 底层替换方案3.2 资源修复3.3 so 修复 1、热修复背景 当发布的版本出现小 Bug 需要及时修复的时候,如果按照传统的方式,这就需要去解决 Bug、测试打包重新发布&

文章目录

      • 1、热修复背景
      • 2、Instant Run 概述
      • 3、类加载
      • 3、热修复
        • 3.1 代码修复
          • 3.1.1 类加载方案
          • 3.1.2 底层替换方案
        • 3.2 资源修复
        • 3.3 so 修复

1、热修复背景

  • 当发布的版本出现小 Bug 需要及时修复的时候,如果按照传统的方式,这就需要去解决 Bug、测试打包重新发布,而用户也需要重新安装你发布的新版本才能解决这个 Bug,使用这个时候可以使用热修复去进行及时修复,而且不需要发布新的版本,只需要发布补丁包,在客户不知不觉间修复掉 Bug
  • 个人认为现在市面上比较成熟稳定的热修复技术方案只有两种:
对比BuglySophix
前世今生腾讯在 Tinker 基础上开发的商业级框架 Bugly阿里的 AndFix 基础上开发的商业级框架 Sophix
及时生效否,仅支持冷启动修复是,支持实时修复和冷启动修复
方法替换
类替换
类结构修改
资源替换/更新替换更新
so 替换/更新替换更新
支持 gradle支持不支持
支持 ART支持支持
支持 Android 7.0支持支持
地址Tinker-GitHub 、Bugly 接入文档Sophix 接入文档

2、Instant Run 概述

  • Instant RunAndroid studio 2.0 以后新增的一个运行机制,能够显著减少开发人员第二次及以后的构建和部署时间。
    传统/Instant Run 编译部署流程图
  • 通过上图可以看出传统的编辑部署需要重新安装和重启 App,这显然会很耗时,而 Instant Run 的构建和部署都是基于更改的部分的,且无需重新安装 App
  • Instant Run 部署有三种方式:
  • (1)Hot swap(热插拔):效率最高的部署,代码的增量改变不需要重启 App,甚至不需要重启当前的 Activity。修改一个现有方法的代码时会采用该方式。
  • (2)Warm swap(热交换)App 不需要重启,但是 Activity 需要重启。修改或删除一个现有的资源文件可采用该方式。
  • (3)Cold swap(冷交换)App 需要重启,但是不需要重新安装。采用该方式的情况很多,例如添加、删除和修改一个字段和方法,添加一个类等。

3、类加载

  • 双亲委派模型的工作流程:当某个类加载器在加载类时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回,只有父类加载器无法完成此加载任务或者没有父类加载器时,才自己去加载。(启动类加载器 > 扩展类加载器 > 应用程序类加载器 > 自定义类加载器)
  • 注意:之所以说双亲委派模式是因为它的一个机制对咱们热修复很重要,那就是避免重复加载,父类已经加载了,则子 ClassLoader 没有必要再次加载
  • Android 平台上虚拟机运行的是Dex字节码,一种对 class 文件优化的产物,Java 源文件可以通过 javac xxx.java 编译生成一个 .class文件,而 Android 是把所有 Class 文件进行合并和优化,然后生成一个最终的 class.dex,目的是把不同 class 文件重复的东西只需保留一份。如果我们的 Android 应用不进行分 dex 处理,这样一个应用的 apk 只会有一个 dex 文件。
  • Android中常用的有两种类加载器,DexClassLoaderPathClassLoader,它们都继承于 BaseDexClassLoader。区别在于调用父类构造器时,DexClassLoader 多传了一个 optimizedDirectory 参数,这个目录必须是内部存储路径,用来缓存系统创建的 Dex 文件。而 PathClassLoader 该参数为 null,只能加载内部存储目录的 Dex 文件。所以我们可以用 DexClassLoader 去加载外部的 apk

3、热修复

3.1 代码修复

3.1.1 类加载方案
  • 需要重启:将新旧 apkdiff 操作得到 补丁.dex 文件,再将 补丁.dexbug.dex 做合并操作得到 已修复.dex 文件,再利用 DexClassLoader 类加载器,根据 DexPathListfindClass() 方法,采取运行时反射注入的形式,将 已修复.dex 插入到 dexElements 数组的第 0 个位置,从而实现代码的修复。(代表:Tinker
// /libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
public Class<?> findClass(String name, List<Throwable> suppressed) {for (Element element : dexElements) {Class<?> clazz = element.findClass(name, definingContext, suppressed);if (clazz != null) {return clazz;}}if (dexElementsSuppressedExceptions != null) {suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));}return null;
}
3.1.2 底层替换方案
  • 不会再次加载新类,而是直接在 Native 层修改原有类,限制为不能增减原有类的方法和字段。
  • 通过修改 ART 虚拟机方法的 ArtMethod 结构体,该结构体中包含了 Java 方法的所有信息,包括执行入口、访问权限、所属类和代码执行地址等。像 Sophix 采用的就是替换掉整个 ArtMethod 结构体,从而实现代码的修复。

3.2 资源修复

  • (1)首先将 ActivityThread 中所有 LoadApk 中的 resDir 的值替换成新合成的资源文件路径(获取 Resources 时,会以 LoadApk 中的 resDir 作为 keyResourcesManager 中获取);
  • (2)创建一个新的 AssetManager,并把资源补丁 apk 加载进新的 AssetManager 中;
  • (3)将 ResourcesManager 中所有 Resources 对象中 AssetManager 替换成我们新建的 AssetManager,那么所有的 Resources 对象获取到的都是新合成的资源文件。

3.3 so 修复

  • Android 现在常见且使用的 cpu 架构 armeabi-v7a(32位)arm64-v8a(64位)
  • Android 加载 so 库的两种方式:
  • System.load(String pathName):传进去的参数:so 库在磁盘中的完整路径, 加载一个自定义外部 so 库文件 。
  • System.loadLibrary(String libName):传进去的参数:so 库名称, 表示的 so 库文件,位于 apk 压缩文件中的 libs 目录,最后复制到 apk 安装目录下。
  • 而咱们所说的 so 修复就是利用 System.loadLibrary(xxx) 方法,思路如下:
  • (1)通过网络下载当前手机 cpu 架构对应 so 文件到指定目录;
  • (2)从指定下载的目录复制 copy so 文件到可动态加载的文件目录下(/data/data/packageName);
  • (3)配置 gradle,指定 cpu 架构;
  • (4)load 加载,利用 System.loadLibrary(xxx) 方法。
  • 那么问题来了,System#loadLibrary(libxxx.so) 如何加载 so 库的呢?
  • 利用 PathClassLoader 类加载器,根据 DexPathListfindLibrary() 方法,采取运行时反射注入的形式,在 sdk < 23 时,将我们的补丁 so 库路径插入到 nativeLibraryDirectories 数组的第 0 个位置,在 sdk >= 23 时,将我们的补丁 so 库路径插入到 nativeLibraryPathElements 数组的第 0 个位置,从而达到修复的目的。
// /libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
// sdk < 23 时对应的代码:
private final File[] nativeLibraryDirectories;
public String findLibrary(String libraryName) {String fileName = System.mapLibraryName(libraryName);for (File directory : nativeLibraryDirectories) {String path = new File(directory, fileName).getPath();if (IoUtils.canOpenReadOnly(path)) {return path;}}return null;
}
// sdk >= 23 时对应的代码:
NativeLibraryElement[] nativeLibraryPathElements;
public String findLibrary(String libraryName) {String fileName = System.mapLibraryName(libraryName);for (NativeLibraryElement element : nativeLibraryPathElements) {String path = element.findNativeLibrary(fileName);if (path != null) {return path;}}return null;
}

版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。

原文链接:https://808629.com/195.html

发表评论:

本站为非赢利网站,部分文章来源或改编自互联网及其他公众平台,主要目的在于分享信息,版权归原作者所有,内容仅供读者参考,如有侵权请联系我们删除!

Copyright © 2022 86后生记录生活 Inc. 保留所有权利。

底部版权信息