美文网首页
AndFix热修复原理-手写实现

AndFix热修复原理-手写实现

作者: Lucky胡 | 来源:发表于2020-01-03 13:14 被阅读0次

    AndFix是阿里巴巴的开源软件,缺点:4年没更新了,兼容性问题。
    开源地址
    Tinker原理的核心是:dex的替换,java层的,不支持即时生效,需要应用冷启动。
    AndFix原理核心:方法的替换,native层替换,支持即时生效。

    AndFix原理

    核心:native层结构体ArtMethod,记录了方法所有的信息,包括方法属于的类、访问权限、代码执行入口等信息。

    下面需要学习,为何替换结构体ArtMethod就能实现方法的修改,怎么替换结构体。

    一、基础知识

    网上查看android源码地址:
    http://androidxref.com

    ArtMethod源码

    1、Android编译过程:
    .java -> .class -> .dex
    虚拟机中解释器模式或AOT快速编译模式来执行dex

    在ArtMethod里面有一个结构体PtrSizedFields,描述了一个方法的各种入口,包括解释器模式下的入口、AOT快速编译模式的入口等。

      // Must be the last fields in the method.
      struct PACKED(4) PtrSizedFields {
        // Method dispatch from the interpreter invokes this pointer which may cause a bridge into
        // compiled code.
        void* entry_point_from_interpreter_;
    
        // Pointer to JNI function registered to this method, or a function to resolve the JNI function.
        void* entry_point_from_jni_;
    
        // Method dispatch from quick compiled code invokes this pointer which may cause bridging into
        // portable compiled code or the interpreter.
        void* entry_point_from_quick_compiled_code_;
    
        // Method dispatch from portable compiled code invokes this pointer which may cause bridging
        // into quick compiled code or the interpreter. Last to simplify entrypoint logic.
    #if defined(ART_USE_PORTABLE_COMPILER)
        void* entry_point_from_portable_compiled_code_;
    #endif
      } ptr_sized_fields_;
    

    2、查找方法:双亲委托机制
    双亲委托

    1、汇编器
    直接将汇编语言翻译为机器码,速度最快。
    
    2、编译器
    将高级语言编译成汇编语言,然后用汇编器转换为机器码。
    编译慢,但是运行快。
    编译器编译出来的代码是平台相关的。
    
    3、解释器
    执行时才翻译代码,速度慢。
    

    Java运行
    为了是代码平台无关,java提供了JVM,java虚拟机。JVM是平台相关的。
    java编译器将java编译为.class字节码,然后给JVM翻译为机器码。

    Android代码运行
    .java->.class->.dex 字节码
    dex字节码通过art或dalvik runtime转换为机器码。

    Dalvik是基于JIT编译的引擎。
    从Android4.4后,引入了ART作为运行时,从5.0后ART全面替代dalvik。
    7.0后在ART中添加了一个JIT编译器,可以在运行时持续提高性能。

    Dalvik使用JIT编译,ART使用AOT编译。
    JIT是程序运行时实时将Delvik字节码翻译成机器码,AOT是在程序安装时将dex翻译为机器码。

    JVM vs. DVM

    ART、AOT、DVM、JIT

    下面就需要看如何改变artmethod里方法入口指针,指向新的方法体。

    二、源码阅读:类是如何加载和查找到的

    1、FindClass
    在JNI里有一个方法, env->FindClass(methodName);
    那如何查看该方法的实现源码呢?

    jni的实现源码在:
    http://androidxref.com/5.1.1_r6/xref/art/runtime/jni_internal.cc

    具体过程省略,整个查找流程为:
    1、JNI FindClass
    2、native FindClass
    jni_internal.cc 589
    3、ClassLinker FindClass()
    class_linker.cc 2117
    4、LookupClass() 双亲委托
    class_linker.cc 3348
    5、DefineClass()
    class_linker.cc 2218
    6、LoadClassMembers()
    class_linker.cc 2767
    7、LinkCode()
    class_linker.cc 2627
    8、GetOatMethod()
    oat_file.cc 595
    9、NeedInterperter() 2525
    10、UpdateMethodsCode()
    instrumentation.cc 679

    三、源码阅读:方法是如何找到的

    Android运行时虚拟机启动的入口方法:
    AndroidRuntime.cpp

    //Start the Android runtime.  This involves starting the virtual machine
    // and calling the "static void main(String[] args)" method in the class
    // named by "className".
    void AndroidRuntime::start(const char* className, const Vector<String8>& options){}
    
    虚拟机启动的第一个类是什么:叫做ZygoteInit类。
    [ZygoteInit.java](http://androidxref.com/5.1.1_r6/xref/frameworks/base/core/java/com/android/internal/os/ZygoteInit.java)
    

    在start()方法中,启动了Java虚拟机(987行)。
    在1027行加载了ZygoteInit类的main()方法。

    调用env->GetStaticMethodID方法,该方法是jni_internal.cc里的方法,就是查找类里的方法,然后将art_method强转为jmethodId。

    手写实现AndFix

    项目代码在GitHub上。
    https://github.com/Hujunjob/HotFix

    首先生成一个需要替换方法的类。

    //错误方法
    class Calculator {
        fun calculator(context: Context) {
            val a = 100
            val b = 0
            Toast.makeText(context, "calculator a/b = ${a / b}", Toast.LENGTH_SHORT).show()
        }
    }
    

    修改方法:

    class Calculator_Fix {
        @MethodReplace(className = "com.hujun.hotfix.Calculator",methodName = "calculator")
        fun calculator(context: Context) {
            val a = 100
            val b = 1
            Toast.makeText(context, "calculator a/b = ${a / b}", Toast.LENGTH_SHORT).show()
        }
    }
    

    其中,自定义注解是用来注解用来替换的方法。

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface MethodReplace {
        //要替换的类名
        String className();
        //要替换的方法名
        String methodName();
    }
    

    然后生成一个dex替换文件。
    在build/tmp/kotlin-classes/debug下找到需要替换的.class文件。
    找到dex工具。
    在Android的SDK里面,/Users/junhu/Library/Android/sdk/build-tools,这是我的Android SDK存储路径。
    然后找到dx文件,配置到环境变量里。

    dx --dex --output 需要生成dex的目录和名称 class文件所在的目录

    如果就在class文件所在的目录里,则用.即可

    dx --dex --output out.dex  .
    

    将生成的out.dex拷贝到/sdcard里,待会用这个dex文件来做热更新。
    写一个Dex管理类,用来加载和热替换曾经存在的方法。

    object DexManager {
    //    private val TAG = this::class.java.name.replace("${'$'}Companion","").split(".").last()
    
        fun loadDex(path: String, context: Context) {
            //loadDex(String sourcePathName, String outputPathName, int flags)
            val dexFile =
                DexFile.loadDex(path, File(context.cacheDir, "opt").absolutePath, Context.MODE_PRIVATE)
            if (dexFile != null) {
                for (entry in dexFile.entries()) {
                    val clazz = dexFile.loadClass(entry, context.classLoader)
                    if (clazz != null) {
                        fixClass(clazz)
                    }
                }
            }
        }
    
        private fun fixClass(clazz: Class<Any>) {
            for (method in clazz.declaredMethods) {
                val annotation = method.getAnnotation(MethodReplace::class.java)
                if (annotation != null) {
                    val className = annotation.className
                    val methodName = annotation.methodName
                    if (TextUtils.isEmpty(className) || TextUtils.isEmpty(methodName)) {
                        continue
                    }
                    val clz = Class.forName(className)
                    val bugMethod = clz.getDeclaredMethod(methodName, *method.parameterTypes)
                    JNI.replaceMethod(bugMethod,method)
    //                Log.i("dexmanager", "fixClass: bugmethod=${bugMethod.name},fixedmethod=${method.name},bug class=${clz.name},fixed class=${clazz.name}")
                }
            }
        }
    
    }
    

    其中JNI.replaceMethod()是调用jni方法,其在native层的实现如下:

    #include "art_5_1.h"
    
    using namespace art::mirror;
    
    /*
     * Class:     com_hujun_hotfix_JNI
     * Method:    replaceMethod
     * Signature: (Ljava/lang/reflect/Method;Ljava/lang/reflect/Method;)V
     */
    extern "C" JNIEXPORT void JNICALL Java_com_hujun_hotfix_JNI_replaceMethod
            (JNIEnv *env, jobject, jobject bugMethod_, jobject fixedMethod_) {
        ArtMethod *bugMethod = reinterpret_cast<art::mirror::ArtMethod *>(env->FromReflectedMethod(
                bugMethod_));
        ArtMethod *fixedMethod = reinterpret_cast<art::mirror::ArtMethod *>(env->FromReflectedMethod(
                fixedMethod_));
    
        //首先获得原来坏掉的方法的ClassLoader
        ((Class *) fixedMethod->declaring_class_)->class_loader_ =
                ((Class *) bugMethod->declaring_class_)->class_loader_;
    
        //线程id也赋值下
        ((Class *) fixedMethod->declaring_class_)->clinit_thread_id_ =
                ((Class *) bugMethod->declaring_class_)->clinit_thread_id_;
    
        //为啥状态要减一
        ((Class *) fixedMethod->declaring_class_)->status_ =
                ((Class *) bugMethod->declaring_class_)->status_ - 1;
    
        ((Class *) fixedMethod->declaring_class_)->super_class_ = 0;
    
        //成员替换
        bugMethod->declaring_class_ = fixedMethod->declaring_class_;
        bugMethod->dex_cache_resolved_methods_ = fixedMethod->dex_cache_resolved_methods_;
        bugMethod->access_flags_ = fixedMethod->access_flags_;
        bugMethod->dex_cache_resolved_types_ = fixedMethod->dex_cache_resolved_types_;
        bugMethod->dex_code_item_offset_ = fixedMethod->dex_code_item_offset_;
        bugMethod->method_index_ = fixedMethod->method_index_;
        bugMethod->dex_method_index_ = fixedMethod->dex_method_index_;
    
        bugMethod->ptr_sized_fields_.entry_point_from_interpreter_ =
                fixedMethod->ptr_sized_fields_.entry_point_from_interpreter_;
    
        bugMethod->ptr_sized_fields_.entry_point_from_jni_ =
                fixedMethod->ptr_sized_fields_.entry_point_from_jni_;
    
        bugMethod->ptr_sized_fields_.entry_point_from_quick_compiled_code_ =
    //            fixedMethod->ptr_sized_fields_.entry_point_from_quick_compiled_code_;
    
    }
    

    其中的 "art_5_1.h"头文件是art_method头文件,阿里进行了简化,可以直接在AndFix里获取,不要自己写了。
    https://github.com/alibaba/AndFix/tree/master/jni/art

    native层的代码是核心代码,将老的有bug的方法,重新定向其入口,指向修复过的方法的入口。并且将老方法的其他参数赋值给新方法,比如把classloader给新方法。
    这样在FindClass()时(前面说的双亲委托),在同一个ClassLoader下,调用老方法时,实际调用的就是新方法了。
    由此实现了方法的热更新。

    这里有个问题需要注意,FindClass()时,如果老方法所在的类并没有被加载过,那替换无法成功,因为并没有被放入ClassLoader的缓存里。
    老方法所在的类,在热更新前,务必已经进行new出来过了。

    AndFix的缺点

    最大的缺点就是,兼容性问题。
    1、Android每次升级,ArtMethod的代码都可能被修改,这样在热更新时,很可能替换就失败了。
    2、由于Android是开源的,所有手机厂商都可以对Android代码进行修改,如果修改到了ArtMethod的代码,也会导致热更新替换失败。

    相关文章

      网友评论

          本文标题:AndFix热修复原理-手写实现

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