美文网首页Android学习笔记 - NDK
Android热修复(AndFix原理和实现方式)

Android热修复(AndFix原理和实现方式)

作者: CoderYuZ | 来源:发表于2019-05-24 17:45 被阅读14次

    首先异常的发生一定是在方法中,想修复的话:

    1. 重新安装apk(我是来逗逼的)
    2. 覆盖含有异常方法的类
    3. 覆盖发生异常的方法

    AndFix实现的是可以实现实时修复异常函数,不需要重启APP。
    对于上面2、3的选择,要了解虚拟机执行原理才能选择

    先了解内存中方法区、堆、栈是干什么用的:

    • 方法区:当JVM使用类加载器加载class文件并输入到内存时,会提取class文件的类型信息,然后将这些信息、方法和静态变量放入到方法区中。
    • 堆区:Java程序在运行时创建的类型对象和数组都存储在堆中,JVM会根据new指令在堆中开辟块内存空间,但是堆中开辟的空间并没有人工指令可以回收,而是通过JVM的垃圾回收器负责回收。
    • 栈:存放要执行的方法和局部变量等。每启动一个线程,JVM都会分配一个Java栈给该线程,当该线程调用某个方法时,JVM会根据方法区中该方法的字节码组建一个栈帧,并将该栈帧压入到栈中,方法执行完毕时,JVM会弹出该栈帧并释放。
      假设ClassA类有methodA、methodB两个函数,classA是ClassA的一个实例对象,来张内存图:


      image.png

    类加载和执行流程:

    1. 调用 ClassA classA = new ClassA()的时候执行第一步,JVM会根据对象的class类型在堆中分配一块储存空间,并且指向改类的符号变量(方法区中的每个类都有一个int型的符号变量指向该类)。
    2. 调用classA. methodA()的时候执行第二步,JVM根据方法区中该方法的字节码组建一个栈帧,并将该栈帧压入到栈中。

    选什么修复方式?在哪一步可以完成修复?
    Java特性决定一个class只会加载一次,方法区中的ClassA在第一次被实例化的时候调用,之后再调用new的时候不会重新加载。所以想不需要重启APP就实现修复功能,只能选择3. 覆盖发生异常的方法,从方法表入手,要做的就是修改方法区中异常函数的ArtMethod结构体。


    准备开撸(用JNI是跑不了的),模拟一个AndFix热修复过程。
    页面布局:


    页面

    ClassA中methodA是个异常函数:

    public class ClassA {
    
        public String methorA() {
            throw new RuntimeException("------异常------");
        }
    
        public String methorB() {
            return "我是methorB返回值";
        }
    }
    

    MainActivity:

    public class MainActivity extends AppCompatActivity {
    
        // Used to load the 'native-lib' library on application startup.
        static {
            System.loadLibrary("native-lib");
        }
    
        TextView tv;
        ClassA classA;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            tv = findViewById(R.id.sample_text);
            classA = new ClassA();
        }
    
        public void callMethodA(View view) {
            tv.setText(classA.methorA());
        }
    
        public void callMethodB(View view) {
            tv.setText(classA.methorB());
        }
    
        public void fixMethod(View view) {
    
        }
    }
    

    现在肯定是调用methorB()修改text文字,调methorA()闪退。下面在fixMethod中进行对ClassA中的methorA(),就行修复。
    首先肯定需要一个正确的ClassA文件:

    public class ClassA {
    
        public String methorA() {
    
            return "我是已修复后的methorA返回值";
    
        }
        // 这个不修复,写这里是为了对比。
        public String methorB() {
    
            return "我是已修复后的methorB返回值";
    
        }
    
    }
    

    现在修复文件有了,如果想要修复指定函数,肯定要确定要修复的类名和函数名。这里用注解实现,新建一个注解:

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Replace {
    
        // 要修复的类
        String clazz();
    
        // 要修复的方法
        String  method();
    }
    

    然后把注解加到正确的methorA()上,此时正确的ClassA:

    public class ClassA {
    
        @Replace(clazz = "com.yu.myfix.ClassA", method = "methorA")
        public String methorA() {
    
            return "我是已修复后的methorA返回值";
    
        }
    
        public String methorB() {
    
            return "我是已修复后的methorB返回值";
    
        }
    
    }
    

    正确和错误的类、函数都就绪。


    问题又来了,apk中的类文件是怎么加载到内存方法区的?我们怎么读区正确的ClassA类?
    答:通过DexFlex类 !!!就可以加载dex文件了。
    问题叕来了,dex文件是什么?怎么生存dex文件?
    答:莫慌,先理解为他就是ClassA.java的另一种形式。


    继续:
    正确和错误的类、函数都就绪,现在可以实现修复过程了,这里的修复包就不写网络下载了,直接放到sdcard里。
    修复的过程新建一个FixManager 类,在MainActivity调用:

        public void fixMethod(View view) {
            File file = new File(Environment.getExternalStorageDirectory(), "classAfix.dex");
            FixManager dexManager = new FixManager(this);
            dexManager.load(file);
        }
    

    FixManager类:

    public class FixManager {
    
        Context context;
    
        public FixManager(Context context) {
            this.context = context;
        }
    
        /**
         * 读取文件,遍历dex文件中的类
         * @param file
         */
        public void load(File file){
            try {
                DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),
                        new File(context.getCacheDir(), "opt").getAbsolutePath(), Context.MODE_PRIVATE);
    
                // 遍历dex文件中的类
                Enumeration<String> entry=dexFile.entries();
                while (entry.hasMoreElements()) {
                    String clazzName= entry.nextElement();
                    Class realClazz= dexFile.loadClass(clazzName, context.getClassLoader());
                    if (realClazz != null) {
                        // 修复类
                        fixClazz(realClazz);
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
    
        /**
         * 修复类中加了特定注解的方法
         * @param clazz
         */
        public void fixClazz(Class clazz){
            //获取类中的方法
            Method[] methods= clazz.getMethods();
    
            for (Method rightMethod : methods) {
    
                //筛选添加了Replace注解的方法
                Replace replace = rightMethod.getAnnotation(Replace.class);
                if (replace == null) {
                    continue;
                }
                // 获取注解中的类名和方法名
                String clazzName=replace.clazz();
                String methodName=replace.method();
    
                try {
                    // 从内存中获取已经加载过的错误类
                    Class wrongClazz = Class.forName(clazzName);
                    // 获取错误类的错误方法
                    Method wrongMethod = wrongClazz.getDeclaredMethod(methodName, rightMethod.getParameterTypes());
                    // 替换方法
                    replace(wrongMethod, rightMethod);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    
        /**
         * native函数,替换错误方法
         * @param wrongMethod
         * @param rightMethod
         */
        private native void replace(Method wrongMethod, Method rightMethod);
    
    }
    

    FixManager中其实就是读取修复包(刚才说了可以把classAfix.dex文件先理解成ClassA.java的另一种形式),遍历其中的类,遍历类中的函数,看看有没有需要修复的函数,判断的依据就是方法是否有我们刚才添加的注解,一切正常的话,应该是拿到了正确的methorA,因为methorB并没有添加注解。
    然后就需要实现FixManager 中replace这个native函数了。
    native-lib.cpp:

    #include <jni.h>
    #include <string>
    #include "art_method.h"
    
    // 这个函数没用,创建C++项目时AS自动生成的示例
    extern "C" JNIEXPORT jstring JNICALL
    Java_com_yu_myfix_MainActivity_stringFromJNI(
            JNIEnv* env,
            jobject /* this */) {
        std::string hello = "Hello from C++";
        return env->NewStringUTF(hello.c_str());
    }
    
    
    extern "C"
    JNIEXPORT void JNICALL
    Java_com_yu_myfix_FixManager_replace(JNIEnv *env, jobject instance, jobject wrongMethod,
                                         jobject rightMethod) {
    
        art::mirror::ArtMethod *wrong=  (art::mirror::ArtMethod *)env->FromReflectedMethod(wrongMethod);
        art::mirror::ArtMethod *right=  (art::mirror::ArtMethod *)env->FromReflectedMethod(rightMethod);
    
        wrong->declaring_class_ = right->declaring_class_;
        wrong->dex_cache_resolved_methods_ = right->dex_cache_resolved_methods_;
        wrong->access_flags_ = right->access_flags_;
        wrong->dex_cache_resolved_types_ = right->dex_cache_resolved_types_;
        wrong->dex_code_item_offset_ = right->dex_code_item_offset_;
        wrong->method_index_ = right->method_index_;
        wrong->dex_method_index_ = right->dex_method_index_;
    
    }
    

    我的哥,这是啥?
    我的弟,哥也解释不太清楚这些变量具体是干啥用的,哥只能告诉你这就是上面提到的ArtMethod结构体,它就是长这样的,就像系统的类你不一定非得知道他的成员变量都是干啥的,但是知道的话,肯定很牛X!(也许你就可以尝试实现一下Sophix这里的逻辑了)
    我的哥,你咋知道这样写?
    我的弟,哥是看系统源码知道的(其实哥是看见AndFix源码差不多就这样写的)。


    上面这段native代码其实就是替换方法,把错误的methodA换成正确的methodA,但是不能直接写wrongMethodA = rightMethodA, 要把wrongMethodA里面的变量替换成rightMethodA里的变量。直接上面这样写编译不通过,因为找不到ArtMethod.h。我们去系统源码中找到这个ArtMethod.h文件copy过来,也就知道了要替换那些变量。
    系统源码可以自己找资源下载,也可以用在线系统源码
    这个文件5.0在/art/runtime/mirror/art_method.h,9.0在/art/runtime/art_method.h
    其他版本反正大概就在这里吧。
    直接copy过来发现这里代码很多,而且其中引入了其他.h。如果你在继续copy其他的,最后可能会把Android系统全copy过来- -!
    .h文件不会被编译到apk中,所以我们只要有art_method.h文件和它里面的一些变量声明,保证我们的项目可以编译通过就可以了,直接把art_method.h文件copy过来之后一顿删,最后的结果:

    namespace art {
    
    namespace mirror {
    
    class  ArtMethod : public Object {
    
    
    public:
    
    
        // Field order required by test "ValidateFieldOrderOfJavaCppUnionClasses".
        // The class we are a part of.
        uint32_t declaring_class_;
    
        // Short cuts to declaring_class_->dex_cache_ member for fast compiled code access.
        uint32_t dex_cache_resolved_methods_;
    
        // Short cuts to declaring_class_->dex_cache_ member for fast compiled code access.
        uint32_t dex_cache_resolved_types_;
    
        // Method dispatch from the interpreter invokes this pointer which may cause a bridge into
        // compiled code.
        uint64_t entry_point_from_interpreter_;
    
        // Pointer to JNI function registered to this method, or a function to resolve the JNI function.
        uint64_t entry_point_from_jni_;
    
        // Method dispatch from quick compiled code invokes this pointer which may cause bridging into
        // portable compiled code or the interpreter.
        uint64_t entry_point_from_quick_compiled_code_;
    
        // Pointer to a data structure created by the compiler and used by the garbage collector to
        // determine which registers hold live references to objects within the heap. Keyed by native PC
        // offsets for the quick compiler and dex PCs for the portable.
        uint64_t gc_map_;
    
        // Access flags; low 16 bits are defined by spec.
        uint32_t access_flags_;
    
        /* Dex file fields. The defining dex file is available via declaring_class_->dex_cache_ */
    
        // Offset to the CodeItem.
        uint32_t dex_code_item_offset_;
    
        // Index into method_ids of the dex file associated with this method.
        uint32_t dex_method_index_;
    
        /* End of dex file fields. */
    
        // Entry within a dispatch table for this method. For static/direct methods the index is into
        // the declaringClass.directMethods, for virtual methods the vtable and for interface methods the
        // ifTable.
        uint32_t method_index_;
    
    };
    
    }  // namespace mirror
    }  // namespace art
    

    然后Object还会报错,这个很显然不能删。。要保留。我们采取同样的copy删减方式对待Object.h(删的更多,有用的就两行,直接写在art_method.h里了)。
    art_method.h如下:

    namespace art {
    
    namespace mirror {
    class Object {
    public:
        uint32_t klass_;
    
        uint32_t monitor_;
    };
    
    class  ArtMethod : public Object {
    
    
    public:
    
    
        // Field order required by test "ValidateFieldOrderOfJavaCppUnionClasses".
        // The class we are a part of.
        uint32_t declaring_class_;
    
        // Short cuts to declaring_class_->dex_cache_ member for fast compiled code access.
        uint32_t dex_cache_resolved_methods_;
    
        // Short cuts to declaring_class_->dex_cache_ member for fast compiled code access.
        uint32_t dex_cache_resolved_types_;
    
        // Method dispatch from the interpreter invokes this pointer which may cause a bridge into
        // compiled code.
        uint64_t entry_point_from_interpreter_;
    
        // Pointer to JNI function registered to this method, or a function to resolve the JNI function.
        uint64_t entry_point_from_jni_;
    
        // Method dispatch from quick compiled code invokes this pointer which may cause bridging into
        // portable compiled code or the interpreter.
        uint64_t entry_point_from_quick_compiled_code_;
    
        // Pointer to a data structure created by the compiler and used by the garbage collector to
        // determine which registers hold live references to objects within the heap. Keyed by native PC
        // offsets for the quick compiler and dex PCs for the portable.
        uint64_t gc_map_;
    
        // Access flags; low 16 bits are defined by spec.
        uint32_t access_flags_;
    
        /* Dex file fields. The defining dex file is available via declaring_class_->dex_cache_ */
    
        // Offset to the CodeItem.
        uint32_t dex_code_item_offset_;
    
        // Index into method_ids of the dex file associated with this method.
        uint32_t dex_method_index_;
    
        /* End of dex file fields. */
    
        // Entry within a dispatch table for this method. For static/direct methods the index is into
        // the declaringClass.directMethods, for virtual methods the vtable and for interface methods the
        // ifTable.
        uint32_t method_index_;
    
    };
    
    }  // namespace mirror
    }  // namespace art
    

    这里的变量,其实就是上面替换方法时需要替换的变量。
    到这里已经可以在5.0的系统中愉快的修复了。


    为什么是5.0?
    因为我们的art_method.h和Object都是从5.0的系统源码中copy来的,不同版本的系统,这俩文件还不一样 - -!, 看下AndFix的源码目录:


    AndFix的源码目录

    尴尬吧。。每个版本可能都要适配,这也是AndFix的缺点。所以到7.0就不维护了,换成了Sophix
    这里也贴一下9.0适配文件
    native-lib.cpp:

    #include <jni.h>
    #include <string>
    #include "art_method_9_0_0.h"
    
    // 这个函数没用,创建C++项目时AS自动生成的示例
    extern "C" JNIEXPORT jstring JNICALL
    Java_com_yu_myfix_MainActivity_stringFromJNI(
            JNIEnv* env,
            jobject /* this */) {
        std::string hello = "Hello from C++";
        return env->NewStringUTF(hello.c_str());
    }
    
    
    extern "C"
    JNIEXPORT void JNICALL
    Java_com_yu_myfix_FixManager_replace(JNIEnv *env, jobject instance, jobject wrongMethod,
                                         jobject rightMethod) {
    
        art::ArtMethod *wrong=  (art::ArtMethod *)env->FromReflectedMethod(wrongMethod);
        art::ArtMethod *right=  (art::ArtMethod *)env->FromReflectedMethod(rightMethod);
    
        wrong->declaring_class_ = right->declaring_class_;
        wrong->ptr_sized_fields_.data_ = right->ptr_sized_fields_.data_;
        wrong->access_flags_ = right->access_flags_;
        wrong->ptr_sized_fields_.entry_point_from_quick_compiled_code_ = right->ptr_sized_fields_.entry_point_from_quick_compiled_code_;
        wrong->dex_code_item_offset_ = right->dex_code_item_offset_;
        wrong->method_index_ = right->method_index_;
        wrong->dex_method_index_ = right->dex_method_index_;
    
    }
    

    art_method_9_0_0.h:

    
    namespace art {
        class ArtMethod {
        public:
    
            // Field order required by test "ValidateFieldOrderOfJavaCppUnionClasses".
            // The class we are a part of.
            uint32_t declaring_class_;
            // Access flags; low 16 bits are defined by spec.
            uint32_t access_flags_;
            /* Dex file fields. The defining dex file is available via declaring_class_->dex_cache_ */
            // Offset to the CodeItem.
            uint32_t dex_code_item_offset_;
            // Index into method_ids of the dex file associated with this method.
            uint32_t dex_method_index_;
            /* End of dex file fields. */
            // Entry within a dispatch table for this method. For static/direct methods the index is into
            // the declaringClass.directMethods, for virtual methods the vtable and for interface methods the
            // ifTable.
            uint16_t method_index_;
    
            // The hotness we measure for this method. Incremented by the interpreter. Not atomic, as we allow
            // missing increments: if the method is hot, we will see it eventually.
            uint16_t hotness_count_;
            // Fake padding field gets inserted here.
            // Must be the last fields in the method.
            // PACKED(4) is necessary for the correctness of
            // RoundUp(OFFSETOF_MEMBER(ArtMethod, ptr_sized_fields_), pointer_size).
            struct PtrSizedFields {
                // Depending on the method type, the data is
                //   - native method: pointer to the JNI function registered to this method
                //                    or a function to resolve the JNI function,
                //   - conflict method: ImtConflictTable,
                //   - abstract/interface method: the single-implementation if any,
                //   - proxy method: the original interface method or constructor,
                //   - other methods: the profiling data.
                void* data_;
    
                // Method dispatch from quick compiled code invokes this pointer which may cause bridging into
                // the interpreter.
                void* entry_point_from_quick_compiled_code_;
            } ptr_sized_fields_;
    
        };
    }
    

    这俩文件替换了,就可以在9.0中愉快的修复了。这里主要的区别会发现是这些结构体中的变量大小不一样。如果不进行版本适配会导致内存乱掉了,Sopfix应该是对这进了内存对齐,具体咋搞的不知道(Sopfix不开源)。

    还有个事没说,classAfix.dex文件咋来的?
    Android的sdk中build-tools提供了用.class文件生成.dex文件的工具:

    dx工具目录
    用法baidu或google,很简单。配置目录到环境变量到dx工具目录下,在终端直接输入dex可以查看帮助。
    使用dx --dex --output=输出文件名 .class文件路径
    比如上面项目,我的文件在:
    image.png
    使用:
    dx --dex --output=classAfix.dex /Users/zhangyu/Desktop/tofix
    上图中classAfix.dex就是生成后的
     
     
    项目地址

    相关文章

      网友评论

        本文标题:Android热修复(AndFix原理和实现方式)

        本文链接:https://www.haomeiwen.com/subject/gqjgzqtx.html