众所周知, Andfix是在native层替换artMethod指针对应的结构体内容实现的Java代码热修复。
那么能用Java实现Andfix的功能吗? 看过本文, 你至少能掌握2个黑科技!
1、 Java也支持类似于C/C++的memcpy即字节拷贝功能!
2、 Andfix的核心artMethod数据替换可以用Java实现!
先回顾一下Andfix的基本原理:
1、 andfix加载补丁包,并修复*.class, fixClass是修复类函数。
public synchronized void fix(File file, ClassLoader classLoader,
List classes) {
......
final DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),
optfile.getAbsolutePath(), Context.MODE_PRIVATE); //加载补丁包
......
Enumeration entrys = dexFile.entries();
Class clazz = null;
while (entrys.hasMoreElements()) {
String entry = entrys.nextElement();
if (classes != null && !classes.contains(entry)) {
continue;// skip, not need fix
}
clazz = dexFile.loadClass(entry, patchClassLoader);
if (clazz != null) {
fixClass(clazz, classLoader); //热修复类
}
}
......
}
2、findClass的作用是通过编译补丁包的类,通过注解判断是否需要修复并调用replaceMethod方法。
private void fixClass(Class clazz, ClassLoader classLoader) {
Method[] methods = clazz.getDeclaredMethods();
MethodReplace methodReplace;
String clz;
String meth;
for (Method method : methods) {
methodReplace = method.getAnnotation(MethodReplace.class); //要修复的方法要添加注解@MethodReplace
if (methodReplace == null)
continue;
clz = methodReplace.clazz(); //类名完整路径(带包名)
meth = methodReplace.method(); //被替换的方法名称
if (!isEmpty(clz) && !isEmpty(meth)) {
replaceMethod(classLoader, clz, meth, method); //核心点:native层替换artMethod结构体
}
}
}
extern void __attribute__ ((visibility ("hidden"))) art_replaceMethod(
JNIEnv* env, jobject src, jobject dest) {
if (apilevel > 23) {
replace_7_0(env, src, dest);
} else if (apilevel > 22) {
replace_6_0(env, src, dest);
} else if (apilevel > 21) {
replace_5_1(env, src, dest);
} else if (apilevel > 19) {
replace_5_0(env, src, dest);
}else{
replace_4_4(env, src, dest);
}
}
4、 这段代码是Andfix的核心, 即替换ArtMethod结构体内容; 因为是按字段逐个替换, 所有有了上面函数的判断android系统版本的过程。
void replace_6_0(JNIEnv* env, jobject src, jobject dest) { art::mirror::ArtMethod* smeth = (art::mirror::ArtMethod*) env->FromReflectedMethod(src); //得到原函数对应的位置,即ArtMethod指针 art::mirror::ArtMethod* dmeth = (art::mirror::ArtMethod*) env->FromReflectedMethod(dest); //得到目标函数对应的位置 ...... //这段代码是Andfix的核心,按照字段逐个赋值实现;假如google或手机厂商修复了ArtMethod的结构,那么Andfix就出现未知问题。 //这就是Andfix所谓的适配问题, 不过阿里最新的Sophix优化了这个过程,使用C/C++常用的memcpy函数,从而不再有适配问题! smeth->declaring_class_ = dmeth->declaring_class_; smeth->dex_cache_resolved_methods_ = dmeth->dex_cache_resolved_methods_; smeth->dex_cache_resolved_types_ = dmeth->dex_cache_resolved_types_; 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_; ...... }
下面放大招了,我用Java实现的代码热修复。 有图有真相, 点击“HOOK”按钮会替换“原方法”按钮的响应函数。 PS:没找到mac录手机gif的方法,只能先用手机录个mp4(命令adb shell screenrecord /sdcard/andfix.mp4),然后再拍成gif
//被替换的方法仅支持public和protected, 需要native层修复访问权限
public void onClickBtn1() {
Toast.makeText(this, "点击原方法", Toast.LENGTH_SHORT).show();
}
private void onClickBtn2() {
try {
HookUtils.hookMethod(this.getClass().getDeclaredMethod("onClickBtn1"),
this.getClass().getDeclaredMethod("hookResult"));
} catch (Exception ex) {
ex.printStackTrace();
}
Toast.makeText(this, "hook原方法", Toast.LENGTH_SHORT).show();
}
private void hookResult() {
Toast.makeText(this, "你调用的是hook方法", Toast.LENGTH_SHORT).show();
}
1、 通过反射调用方法和直接调用方法是一样的, 可以查看反射的调用逻辑看出端倪。 反射时都要先渠道Method引用, 其父类的artMethod变量存储的就是ArtMethod对象指针;
2、 Andfix是在Native层修改ArtMethod对象内容, 我现在能够拿到这个指针, 那么用Java替换ArtMethod内容不久行了。 PS: Java不知道ArtMethod对象包含哪些变量,更不知道它的大小, 该怎么办?
3、 通过查看native代码可以发现每个ArtMethod大小一致,就是个线性数组, 即Java类的每个方法在虚拟机里的ArtMethod类对象占用空间一致。 PS:ArtMethod就是个索引,虚拟机通过它找到真正的函数体。
public class SizeUtils {
private void method1() {
}
private void method2() {
}
/**
* 因为Java的每个方法有一个Native层的artMethod指针,虚拟机通过artMethod指针寻址方法;
* 而Native层是线性数组且每个ArtMethod对象占用内存相等,所以数组相邻元素地址的差就是元素大小。
* @return native层ArtMethod对象大小(单位:字节)
*/
public static long getArtMethodSize() {
long size = 0;
try {
Method method1 = SizeUtils.class.getDeclaredMethod("method1");
method1.setAccessible(true);
Field field1 = Method.class.getSuperclass().getDeclaredField("artMethod");
field1.setAccessible(true);
Long size1 = (Long) field1.get(method1); //field1是method1方法在native层的指针
Method method2 = SizeUtils.class.getDeclaredMethod("method2");
method2.setAccessible(true);
Field field2 = Method.class.getSuperclass().getDeclaredField("artMethod");
field2.setAccessible(true);
Long size2 = (Long) field2.get(method2); //field2是method2方法在native层的指针
size = size2 - size1;
} catch (Exception ex) {
ex.printStackTrace();
}
return size;
}
}
PS: Java做实时热修复有个坑, 被替换的方法必须是public或protected, 如果是被修复的是private方法会报错。 需要在native层修改访问权限变量access_flag, Java修改这个值没用!
源码: http://download.csdn.net/download/brycegao321/9896342