阿里热修复Andfix的Java实现

         众所周知, 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结构体
            }
        }
    }


3、 replacMethod调用的是native方法,andfix为了适配要判断android版本(存在适配问题,如果厂商修改了ArtMethod结构,会产生不可预知的后果)。 而最新的Sophix优化了这个函数,通过memcpy实现artMethod内容替换功能; 后面我要讲的就是参考Sophix的memcpy作法,使用Java来实现。

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


                                     阿里热修复Andfix的Java实现_第1张图片

    //被替换的方法仅支持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();
    }

       下图说明了用Java实现代码实时热修复的原理:

1、 通过反射调用方法和直接调用方法是一样的, 可以查看反射的调用逻辑看出端倪。 反射时都要先渠道Method引用, 其父类的artMethod变量存储的就是ArtMethod对象指针;

2、 Andfix是在Native层修改ArtMethod对象内容, 我现在能够拿到这个指针, 那么用Java替换ArtMethod内容不久行了。 PS: Java不知道ArtMethod对象包含哪些变量,更不知道它的大小, 该怎么办?

3、 通过查看native代码可以发现每个ArtMethod大小一致,就是个线性数组, 即Java类的每个方法在虚拟机里的ArtMethod类对象占用空间一致。 PS:ArtMethod就是个索引,虚拟机通过它找到真正的函数体。


阿里热修复Andfix的Java实现_第2张图片


4、 反射出Method的artMethod变量即可得到指针, 那么一个ArtMethod占用多大空间呢? 因为native层是地址连续的数组, 而artMethod就是每个方法的指针, 那么相邻2个函数的地址差不就是大小了?    定义一个Java类,声明2个函数然后取2个函数的地址差

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;
    }
}

5、有了指针artMethod和结构体ArtMethod的大小, 现在的问题是如何将一个地址的数据拷贝到另一个地址。 我们知道Java没提供字节拷贝API, 这时黑科技闪亮登场了。 libcore.io.Memory 类提供了字节读写的方法peekByte和pokeByte, 能够实现memcpy功能


    PS: Java做实时热修复有个坑, 被替换的方法必须是public或protected, 如果是被修复的是private方法会报错。 需要在native层修改访问权限变量access_flag, Java修改这个值没用!


          源码: http://download.csdn.net/download/brycegao321/9896342






你可能感兴趣的:(Android)