应用场景
解决的问题
- 应用发布后出现bug,修复流程又要经过开发、测试、灰度、发布整个链路,流程周期比较长,代价比较大。
- 比较小的改动或需要立即生效的功能,想要立即触达用户,整个链路成本比较高。
解决的范围
- Android中Java代码的热修复
方式1:自定义ClassLoader
Java ClassLoader双亲委派模型
- Java类的双亲委派原理:某个类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
protected Class loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
long t1 = System.nanoTime();
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
- Android中类的加载原理:和Java类的加载机制基本一致,Java类将代码编译成class文件,JVM加载class文件;而Android多出的一步就是将class文件转换为dex文件,通过dalvik或者Art虚拟机加载,Android也有自己的类加载器。每个 Class 对象的内部都有一个 classLoader 字段来标识自己是由哪个 ClassLoader 加载的。
class Class {
...
private transient ClassLoader classLoader;
...
}
Android中的classloader
ClassLoader是一个抽象类,而它的具体实现类主要有:
- BootClassLoader:用于加载Android Framework层class文件。
- PathClassLoader:用于Android应用程序类加载器。可以加载指定的dex,以及jar、zip、apk中的classes.dex
- DexClassLoader:用于加载指定的dex,以及jar、zip、apk中的classes.dex
他们之间的继承关系是:
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
}
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);
}
}
自定义ClassLoader
- 思路:利用双亲委派的原理,父ClassLoader会优先加载,因此创建父ClassLoader用于加载patch类。
- 方法: 创建自定义的ClassLoader用于加载补丁class,自定义ClassLoader继承于BaseDexClassLoader,并使DexClassLoader和PathClassLoader继承于自定义ClassLoader。这样在load class时,会优先调用自定义ClassLoader去加载类。
- 具体代码:
- 1. 创建自定义ClassLoader,定义class文件路径
ClassLoader customClassLoader = new PathClassLoader("/sdcard/xx.dex", getClassLoader())
- 2. findClass()定义加载class方法
public DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory) {
//.........
// splitDexPath 实现为返回 List.add(dexPath)
// makeDexElements 会去 List.add(dexPath) 中使用DexFile加载dex文件返回 Element数组
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext);
//.........
}
public Class findClass(String name, List suppressed) {
//从element中获得代表Dex的 DexFile
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
//查找class
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
安全问题
- 问题:如果两个相关联的类在不同的dex中就会报错,这个校验是因为直接引用到的类(第一层级关系,不会进行递归搜索)和clazz都在同一个dex中的话,那么这个类就会被打上CLASS_ISPREVERIFIED标志:
- 解决方法:
- 防止类被打上CLASS_ISPREVERIFIED标志,需要在patch往所有类的构造函数里面插入了一段代码:
if (ClassVerifier.PREVENT_VERIFY) {
System.out.println(AntilazyLoad.class);
}
这样当安装apk的时候,classes.dex内的类都会引用一个其他dex中的AntilazyLoad类,这样就防止了类被打上
局限性
- 可能需要重启应用:当app运行到一半时,所需发生变更的类已经被加载过,而在Android上无法对一个类进行卸载操作,若不重启,原来的类还存储于虚拟机中,新类无法被加载。
- 去除CLASS_ISPREVERIFIED标志,会导致性能轻微下降。
方式2:插桩式
热修复流程
- 插桩式与自定义ClassLoader的原理类似,但没有创建自定义ClassLoader,而是利用PathClassLoader可以按顺序加载dex文件的特点,将patch.dex插入到数组中第一个位置加载。
- 如下图:在PathClassLoader中的Element数组为:[patch.dex , classes.dex , classes2.dex]。如果存在Key.class位于patch.dex与classes2.dex中都存在一份,当进行类查找时,循环获得dexElements中的DexFile,查找到了Key.class则立即返回,不会再管后续的element中的DexFile是否能加载到Key.class了。
Element数组操作
PathClassLoader,继承自 BaseDexClassLoader,在 BaseDexClassLoader 里,有一个 DexPathList 变量,在 DexPathList 的实现里,有一个 Element[] dexElements 变量,这里面保存了所有的 dex,下面是系统
public class PathClassLoader extends BaseDexClassLoader {
}
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
}
final class DexPathList {
// 保存了 dex 的列表
private Element[] dexElements;
public Class findClass(String name, List suppressed) {
// 遍历 dexElements
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
// 从 DexFile 中查找 Class
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
// ...
return null;
}
}
具体实现的方法
public static void injectDexAtFirst(String dexPath, String defaultDexOptPath) throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
// 创建补丁 dex 的 classloader,目的是使用其中的补丁 dexElements
DexClassLoader dexClassLoader = new DexClassLoader(dexPath, defaultDexOptPath, dexPath, getPathClassLoader());
// 获取到旧的 classloader 的 pathlist.dexElements 变量
Object baseDexElements = getDexElements(getPathList(getPathClassLoader()));
// 获取到补丁 classloader 的 pathlist.dexElements 变量
Object newDexElements = getDexElements(getPathList(dexClassLoader));
// 将补丁 的 dexElements 插入到旧的 classloader.pathlist.dexElements 前面
Object allDexElements = combineArray(newDexElements, baseDexElements);
}
private static PathClassLoader getPathClassLoader() {
PathClassLoader pathClassLoader = (PathClassLoader) InsertDexUtils.class.getClassLoader();
return pathClassLoader;
}
private static Object getDexElements(Object paramObject)
throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException {
return Reflect.on(paramObject).get("dexElements");
}
private static Object getPathList(Object baseDexClassLoader)
throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
return Reflect.on(baseDexClassLoader).get("pathList");
}
private static Object combineArray(Object firstArray, Object secondArray) {
Class localClass = firstArray.getClass().getComponentType();
int firstArrayLength = Array.getLength(firstArray);
int allLength = firstArrayLength + Array.getLength(secondArray);
Object result = Array.newInstance(localClass, allLength);
for (int k = 0; k < allLength; ++k) {
if (k < firstArrayLength) {
Array.set(result, k, Array.get(firstArray, k));
} else {
Array.set(result, k, Array.get(secondArray, k - firstArrayLength));
}
}
return result;
}
方式3:底层替换方案
基本思路
与上述java类加载方案不同的是,底层替换方案不会再次加载新类,而是直接在Native层修改原有类,由于是在原有类进行修改限制会比较多,不能够增减原有类的方法和字段,如果增加了方法数,那么方法索引数也会增加,这样访问方法时会无法通过索引找到正确的方法,同样的字段也是类似的情况。底层替换方案直接替换了方法,可以立即生效不需要重启。
注意:该方法只针对Art VM, JVM的底层数据结构与Art VM不同。
replaceMethod原理
虚拟机中类的实现
- java 中的类,方法,变量,对应到虚拟机里的实现是 Class,ArtMethod,ArtField,主要C++代码如下
class Class: public Object {
public:
// ...
// classloader 指针
uint32_t class_loader_;
// 数组的类型表示
uint32_t component_type_;
// 解析 dex 生成的缓存
uint32_t dex_cache_;
// interface table,保存了实现的接口方法
uint32_t iftable_;
// 类描述符,例如:java.lang.Class
uint32_t name_;
// 父类
uint32_t super_class_;
// virtual method table,虚方法表,指令 invoke-virtual 会用到,保存着父类方法以及子类复写或者覆盖的方法,是 java 多态的基础
uint32_t vtable_;
// public private
uint32_t access_flags_;
// 成员变量
uint64_t ifields_;
// 保存了所有方法,包括 static,final,virtual 方法
uint64_t methods_;
// 静态变量
uint64_t sfields_;
// class 当前的状态,加载,解析,初始化等等
Status status_;
static uint32_t java_lang_Class_;
};
class ArtField {
public:
uint32_t declaring_class_;
uint32_t access_flags_;
uint32_t field_dex_idx_;
uint32_t offset_;
};
class ArtMethod {
public:
uint32_t declaring_class_;
uint32_t access_flags_;
// 方法字节码的偏移
uint32_t dex_code_item_offset_;
// 方法在 dex 中的 index
uint32_t dex_method_index_;
// 在 vtable 或者 iftable 中的 index
uint16_t method_index_;
// 方法的调用入口
struct PACKED(4) PtrSizedFields {
ArtMethod** dex_cache_resolved_methods_;
GcRoot* dex_cache_resolved_types_;
void* entry_point_from_jni_;
void* entry_point_from_quick_compiled_code_;
} ptr_sized_fields_;
};
Class 中的 iftable_,vtable_,methods_ 里面保存了所有的类方法,sfields_,ifields_ 保存了所有的成员变量。而在 ArtMethod 中,ptr_sized_fields_ 变量指向了方法的调用入口,也就是执行字节码的地方。在虚拟机内部,调用一个方法的时候,可以简单的理解为会找到 ptr_sized_fields_ 指向的位置,跳转过去执行对应的方法字节码或者机器码
Method替换原理
每次调用方法的时候,都是通过 ArtMethod 找到方法,然后跳转到其对应的字节码/机器码位置去执行,那么我们只要更改了跳转的目标位置,那么自然方法的实现也就被改变了
实现代码
- 找到要被替换的旧方法和新方法,可以在java层直接通过反射获取
// 创建补丁的 ClassLoader
pluginClassLoader = DexClassLoader(pluginPath, dexOutPath.absolutePath, nativeLibDir.absolutePath, this::class.java.classLoader)
// 通过补丁 ClassLoader 加载新方法
val toMethod = pluginClassLoader.loadClass("com.zy.hotfix.native_hook.PatchNativeHookUtils").getMethod("getMsg")
// 反射获取到需要修改的旧方法
val fromMethod = nativeHookUtils.javaClass.getMethod("getMsg")
- 调用native的C++方法替换ArtMethod 内容
nativeHookUtils.patch(fromMethod, toMethod)
Java_com_zy_hotfix_native_1hook_NativeHookUtils_patch(JNIEnv* env, jobject clazz, jobject src, jobject dest) {
// 获取到 java 方法对应的 ArtMethod
art::mirror::ArtMethod* smeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(src);
art::mirror::ArtMethod* dmeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(dest);
reinterpret_cast(dmeth->declaring_class_)->clinit_thread_id_ =
reinterpret_cast(smeth->declaring_class_)->clinit_thread_id_;
reinterpret_cast(dmeth->declaring_class_)->status_ =
static_cast(reinterpret_cast(smeth->declaring_class_)->status_ -1);
//for reflection invoke
reinterpret_cast(dmeth->declaring_class_)->super_class_ = 0;
// 替换方法中的内容
smeth->declaring_class_ = dmeth->declaring_class_;
smeth->access_flags_ = dmeth->access_flags_ | 0x0001;
smeth->dex_code_item_offset_ = dmeth->dex_code_item_offset_;
smeth->dex_method_index_ = dmeth->dex_method_index_;
smeth->method_index_ = dmeth->method_index_;
smeth->hotness_count_ = dmeth->hotness_count_;
// 替换方法的入口
smeth->ptr_sized_fields_.dex_cache_resolved_methods_ =
dmeth->ptr_sized_fields_.dex_cache_resolved_methods_;
smeth->ptr_sized_fields_.dex_cache_resolved_types_ =
dmeth->ptr_sized_fields_.dex_cache_resolved_types_;
smeth->ptr_sized_fields_.entry_point_from_jni_ =
dmeth->ptr_sized_fields_.entry_point_from_jni_;
smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_ =
dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_;
}
优缺点
优点:
- 补丁可以实时生效,不需要重启应用
缺点:
兼容性问题:
- 问题:每个版本的ArtMethod结构体不同,厂商可以改造结构体
- 解决方法:对ArtMethod结构体的内存整个替换,使用memcpy(smeth,dmeth, sizeof(ArtMethod))。但sizeof是编译期决定,不能运行时获取。但一个类中的ArtMethod的内存分配是连续的,可以通过两个连续方法地址差值确定sizeof(ArtMethod)