1、热修复背景
- 当发布的版本出现小
Bug
需要及时修复的时候,如果按照传统的方式,这就需要去解决 Bug
、测试打包重新发布,而用户也需要重新安装你发布的新版本才能解决这个 Bug
,使用这个时候可以使用热修复去进行及时修复,而且不需要发布新的版本,只需要发布补丁包,在客户不知不觉间修复掉 Bug
。 - 个人认为现在市面上比较成熟稳定的热修复技术方案只有两种:
对比 | Bugly | Sophix |
---|
前世今生 | 腾讯在 Tinker 基础上开发的商业级框架 Bugly | 阿里的 AndFix 基础上开发的商业级框架 Sophix |
及时生效 | 否,仅支持冷启动修复 | 是,支持实时修复和冷启动修复 |
方法替换 | 是 | 是 |
类替换 | 是 | 是 |
类结构修改 | 是 | 否 |
资源替换/更新 | 替换 | 更新 |
so 替换/更新 | 替换 | 更新 |
支持 gradle | 支持 | 不支持 |
支持 ART | 支持 | 支持 |
支持 Android 7.0 | 支持 | 支持 |
地址 | Tinker-GitHub 、Bugly 接入文档 | Sophix 接入文档 |
2、Instant Run 概述
Instant Run
是 Android studio 2.0
以后新增的一个运行机制,能够显著减少开发人员第二次及以后的构建和部署时间。
- 通过上图可以看出传统的编辑部署需要重新安装和重启
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
中常用的有两种类加载器,DexClassLoader
和 PathClassLoader
,它们都继承于 BaseDexClassLoader
。区别在于调用父类构造器时,DexClassLoader
多传了一个 optimizedDirectory
参数,这个目录必须是内部存储路径,用来缓存系统创建的 Dex
文件。而 PathClassLoader
该参数为 null
,只能加载内部存储目录的 Dex
文件。所以我们可以用 DexClassLoader
去加载外部的 apk
。
3、热修复
3.1 代码修复
3.1.1 类加载方案
- 需要重启:将新旧
apk
做 diff
操作得到 补丁.dex
文件,再将 补丁.dex
和 bug.dex
做合并操作得到 已修复.dex
文件,再利用 DexClassLoader
类加载器,根据 DexPathList
的 findClass()
方法,采取运行时反射注入的形式,将 已修复.dex
插入到 dexElements
数组的第 0
个位置,从而实现代码的修复。(代表:Tinker
)
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
作为 key
去 ResourcesManager
中获取); - (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
类加载器,根据 DexPathList
的 findLibrary()
方法,采取运行时反射注入的形式,在 sdk < 23
时,将我们的补丁 so
库路径插入到 nativeLibraryDirectories
数组的第 0
个位置,在 sdk >= 23
时,将我们的补丁 so
库路径插入到 nativeLibraryPathElements
数组的第 0
个位置,从而达到修复的目的。
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;
}
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;
}