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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,233评论 6 495
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,357评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,831评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,313评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,417评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,470评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,482评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,265评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,708评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,997评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,176评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,827评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,503评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,150评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,391评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,034评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,063评论 2 352