AndFix热修复原理-手写实现

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的代码,也会导致热更新替换失败。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。