前言
热修复技术是Android开发中的重要技术之一,它能够在不重新发布应用的情况下,修复线上版本的BUG,提升用户体验。本文结合三篇技术博客的内容,对Android热修复的实现原理进行全面总结。
什么是热修复
热修复是指在应用程序运行过程中,动态地修复程序中存在的缺陷,而无需重新安装或重启应用。就像王者荣耀在启动时会检测是否有新的补丁包需要加载一样,热修复能够让用户在无感知的情况下获得修复后的功能。
热修复的应用场景
当APP上线后发现缺陷时,传统的做法是修复bug后重新发布版本,但这种方式时间成本高,用户体验差。热修复通过发布插件补丁,使APP在运行时加载补丁中的代码来解决问题,具有以下优势:
- 无需重新发布APP
- 用户无感知
- 快速修复紧急BUG
Android热修复核心技术原理
1. Java类加载机制 - 双亲委派模型
Java的类加载器(ClassLoader)采用双亲委派模型加载class文件:
- 检查当前ClassLoader是否已加载该class,有则直接返回
- 若未加载,则委托父ClassLoader加载
- 递归向上委托,直到顶层ClassLoader
- 若所有父ClassLoader都未加载,则由当前ClassLoader调用findClass()方法加载
双亲委派模型的优势:
- 类加载的共享功能:Framework层的类一旦被顶层ClassLoader加载,会缓存到内存中,提高加载效率
- 类加载的隔离功能:防止恶意代码冒充核心类库
2. Android运行流程
Android运行流程分为四个步骤:
- Java源码(.java)编译成字节码(.class)
- 打包成.dex文件
- Dalvik/ART虚拟机加载.dex文件
- 加载其中的.class文件到内存中使用
3. Android中的ClassLoader
Android中的常见ClassLoader有四种:
- BootClassLoader:加载Android Framework层的class
- PathClassLoader:加载已安装APK中的class
- DexClassLoader:加载指定目录的class
- BaseDexClassLoader:PathClassLoader和DexClassLoader的父类
4. Element集合与DexPathList
Android热修复的关键在于理解Element数组和DexPathList的关系:
- BaseDexClassLoader包含DexPathList对象
- DexPathList包含Element[] dexElements数组
- 热修复的核心思想是通过反射操作Element数组,将补丁dex插入到数组前面,利用ClassLoader的类加载机制优先加载补丁中的类
主流热修复方案对比
| 框架 | 优点 | 缺点 |
|---|---|---|
| AndFix | 无需重启APP,立即生效 | 存在兼容性问题 |
| Tinker | 兼容性好 | 需要重启APP才能生效 |
AndFix实现原理详解
核心思想
AndFix通过底层替换方案实现热修复,直接替换ArtMethod结构体中的字段,从而达到方法替换的目的。
实现步骤
- 发现并修复BUG,将修复的Java文件编译成class,再打包成dex文件
- 将修复的方法体Method从dex文件中取出,同时取出有问题的方法Method
- 将正确和错误的Method传到底层进行替换操作
- 在底层完成替换
Dalvik与ART虚拟机的区别
- Dalvik:Android 4.4之前的虚拟机,JIT编译器在运行时进行编译,容易卡顿
- ART:Android 4.4之后的虚拟机,将JIT编译过程放在安装时进行,提升了运行效率,采用空间换时间策略
AndFix适配方案
由于Dalvik和ART虚拟机在方法调用和内存管理上存在差异,AndFix需要分别适配:
- Dalvik环境下使用
dalvik_replaceMethod()函数 - ART环境下使用
art_replaceMethod()函数
简单热修复实现示例
方式一:基于ClassLoader的Dex插入方案
public class FixDexUtil {
private static final String DEX_SUFFIX = ".dex";
private static final String APK_SUFFIX = ".apk";
private static final String JAR_SUFFIX = ".jar";
private static final String ZIP_SUFFIX = ".zip";
private static final String DEX_DIR = "odex";
private static final String OPTIMIZE_DEX_DIR = "optimize_dex";
private static HashSet<File> loadedDex = new HashSet<>();
/**
* 加载补丁
*/
public static void loadFixedDex(Context context, File patchFilesDir) {
// dex合并之前的dex
doDexInject(context, loadedDex);
}
/**
* dex注入
*/
private static void doDexInject(Context appContext, HashSet<File> loadedDex) {
String optimizeDir = appContext.getFilesDir().getAbsolutePath() +
File.separator + OPTIMIZE_DEX_DIR;
File fopt = new File(optimizeDir);
if (!fopt.exists()) {
fopt.mkdirs();
}
try {
// 1.加载应用程序dex的Loader
PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader();
for (File dex : loadedDex) {
// 2.加载指定的修复的dex文件的Loader
DexClassLoader dexLoader = new DexClassLoader(
dex.getAbsolutePath(),// 修复好的dex(补丁)所在目录
fopt.getAbsolutePath(),// 存放dex的解压目录(用于jar、zip、apk格式的补丁)
null,// 加载dex时需要的库
pathLoader// 父类加载器
);
// 3.开始合并
// 合并的目标是Element[],重新赋值它的值即可
// BaseDexClassLoader中有变量: DexPathList pathList
// DexPathList中有变量 Element[] dexElements
// 依次反射即可
//3.1 准备好pathList的引用
Object dexPathList = getPathList(dexLoader);
Object pathPathList = getPathList(pathLoader);
//3.2 从pathList中反射出element集合
Object leftDexElements = getDexElements(dexPathList);
Object rightDexElements = getDexElements(pathPathList);
//3.3 合并两个dex数组
Object dexElements = combineArray(leftDexElements, rightDexElements);
// 重写给PathList里面的Element[] dexElements;赋值
Object pathList = getPathList(pathLoader);// 一定要重新获取,不要用pathPathList,会报错
setField(pathList, pathList.getClass(), "dexElements", dexElements);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
方式二:基于AndFix的底层替换方案
// 修复类示例
public class Calculator {
@MethodReplace(clazz = "com.example.Calculator", method = "calculate")
public int calculate() {
int j = 10;
int i = 1; // 修复:原代码为0导致除零异常
int result = j / i;
return result;
}
}
热修复实现关键步骤
-
制作补丁包:
- 修改bug代码
- 使用dx工具将修复的class打包成dex文件
- 命令:
dx --dex --output out.dex BugClass.class
-
下发补丁包:
- 将dex文件上传到服务器
- 客户端下载补丁文件到指定目录
-
加载补丁:
- 应用启动时检测补丁文件
- 通过反射机制将补丁dex插入到Element数组前面
- 重新设置ClassLoader的dexElements
-
验证修复:
- 重启应用后验证bug是否修复
- 对于AndFix等即时生效方案,无需重启即可验证
总结
Android热修复技术是解决线上问题的重要手段,主要有两大实现方案:
- 类加载方案:通过ClassLoader机制插入补丁dex,代表框架如Tinker
- 底层替换方案:通过修改虚拟机底层结构替换方法,代表框架如AndFix
开发者需要根据具体需求选择合适的热修复方案,在兼容性、实时性、稳定性等方面做出权衡。随着Android系统的发展,热修复技术也在不断完善,Sophix等新一代热修复框架解决了早期方案的诸多问题,提供了更好的稳定性和兼容性。
参考资料:
https://blog.csdn.net/qq_39799899/article/details/102478355
Android热修复实现及原理
https://blog.csdn.net/ljx1400052550/article/details/115515676
模仿手写阿里andfix的实现原理:
https://www.jianshu.com/p/d7308d1ca42e