Andfix&Tinker 热修复方案原理

Andfix

andfix从native入手修改ArtMethod的字节码地址实现错误方法块的修复。修复的粒度是方法块字节码引用。

Java的内存分布

内存分布

方法区
JVM读取class文件,提取class的类型信息,并将这些信息存储到方法区,同时该类型的类静态变量也会放到方法区,还有方法表,每个类都会有个方法表。
堆区
主要存放的是对象,Java程序在运行时创建的所有类型的对象和数组都储存在堆中。
JVM会根据new的指令在堆中开辟一个确定类型的对象内存空间,但是堆中开辟的对象空间并没有任何人工指令可以回收,而是通过JVM的垃圾回收机制进行回收。
栈区
每启动一个线程,JVM都会为它创建一个JAVA栈,用于存放方法中的局部变量、操作数等。
当线程调用某个方法时,JVM会根据方法区中该方法的字节码组建一个栈帧,并将该栈帧压入到Java栈中,方法执行完毕后,JVM将该栈帧弹出,并释放掉。

我们写的Java文件被编译成class文件到最终加载到内存,所属的内存区域是方法区,所以要做的就是,将差分包中没有bug的方法替换掉已经被加载到内存方法区中有bug方法。

虚拟机运行机制

虚拟机加载执行ActivityThread
  1. 声明一个ApplicationThread类型的成员变量
//这一步不会将Application.class字节码加载到内存的,会在方法区生成一个int类型的符号表量
private Application application

对象只有在主动引用的情况下才会加载到内存,常见的主动引用方式有:new、反射创建、JNI的findCalss()、序列化、调用类的静态成员变量(final除外)和静态方法、初始化一个类如果其父类没有初始化,会先初始化父类。

  1. 通过反射的方式创建application对象,这一步会将application的字节码文件加载到内存,创建的对象存储在堆区。


    APP启动流程

    当通过Application对象调用onCreate()方法时,堆区的application对象指向int符号变量,int符号变量指向方法表,执行onCreate()方法,将onCreate组建成栈帧,压入Java栈,执行完毕后弹出并释放。

方法表

方法表可以理解为是一个数组,数组中存放的是ArtMethod结构体(Android5.0之前为Method)。ArtMethod中存放的方法入口、字节码地址等信息。

扩展JVM DVM ART

  • JVM基于栈,意味着需要去栈中读写数据,所需要的指令会更多,这样会导致速度变慢,对于性能有限的移动设备显然不合适。
  • DVM是基于寄存器的,它没有基于栈的虚拟机在复制数据时而使用大量的出入栈指令,同事指令更紧凑、简洁。但是由于显式的制定了操作数,所以基于寄存器的指令会比基于栈的指令要大。
  • JVM中,java类被编译成一个或多个.class文件,并打包成.jar文件,而后JVM会通过相应的.class文件和.jar文件获取相应的字节码。
    而DVM会用dx工具把所有的class文件打包成一个.dex文件,然后DVM会从该.dex文件中读取指令和数据。这个.dex文件将所有的.class文件里面所包含的信息全部整合到了一起,这样加载就加快了速度。
  • DVM经过优化,允许在有限的内存中同时运行多个进程。在Android中每一个应用都运行在一个DVM实例中,每一个DVM实例都运行在一个独立的进程空间,独立的进程可以防止虚拟机崩溃时所有程序都被关闭。
  • Zygote是一个DVM进程,同时也用来创建和初始化其他DVM进程。每当系统需要一个应用程序进程的时候,Zygote就会fork自身,快速地创建和初始化一个DVM实例,用于程序运行。
  • DVM拥有预加载-共享机制,不同应用之间运行时可以共享相同的类,拥有更高的效率。而JVM机制不存在这种共享机制。不同的程序,打包以后程序都是彼此独立的,即便是他们使用了相同的类,运行时也都是单独加载和运行的。
  • JVM使用了JIT(Just In Time Compiler),而DVM早期没有使用JIT编译器。早期DVM执行代码,都需要解释器将dex代码编译成机器码,然后交给系统处理,效率不是很高。Android 2.2之后开始为DVM使用了JIT编译器,它会对多次运行的代码(热点代码)进行编译,生成相当精简的本地机器码(Native Code),这样在下次执行到相同的逻辑时,直接使用编译好的机器码即可。需要注意的是,应用程序每次重新运行的时候,都需要重做这个编译工作。

ART虚拟机

  • 前文了解到,DVM中的应用每次运行时,字节码都需要通过JIT编译器编译为机器码,这样会使应用程序的运行效率降低。而在ART中,系统安装应用程序时会进行一次AOT(ahead of time compilation),将字节码预编译成机器码并存储在本地,这样应用程序每次运行时就不需要执行编译了,会大大增加效率。但是AOT不是完美的,它的缺点主要有两个:第一个是AOT会使安装应用的时间变长,尤其是复杂的应用。第二个是字节码预先编译成机器码,机器码需要存储空间会多一些。为了解决这两个问题,Android 7.0版本中的ART加入了JIT即时编译器,作为AOT的一个补充。应用程序安装时并不会将字节码全部编译成机器码,而是在系统运行中将热点代码编译成机器码,从而缩短应用程序安装时间,并且节省内存。

  • DVM是为32位CPU设计的,而ART是支持64位并且兼容32位CPU,这也是DVM被淘汰的主要原因之一。

  • ART对垃圾回收机制进行了改进,比如更频繁的执行并行垃圾收集,将GC暂停由2次减少为1次等等。

  • ART运行时堆空间划分和DVM不同。

手动实现Andfix

  1. 找到要修复的方法块,修复bug代码后,对方法加上注解方便后续定位到修改的方法。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
//a
public @interface Replace {
    //  目的
//    地方  在哪里      14号   多少钱
    String clazz();
    String method();
}
  1. 编译修改的Java文件生成.class,使用SDK的DX工具转成.dex文件放入手机中。
  2. APP加载dex文件,遍历出dex中所有的Class后,使用之前的注解找到要修改的function
 DexFile dexFile = DexFile.loadDex()
Enumeration<String> entry= dexFile.entries();
while (entry.hasMoreElements()) {
//                全类名
  String className = entry.nextElement();
  Class realClazz=dexFile.loadClass(className, context.getClassLoader());
  if (realClazz != null) {
    fixClass(realClazz);
  }
}

    private void fixClass(Class realClazz) {
//加载方法 Method
        Method[] methods = realClazz.getMethods();
        for (Method rightMethod : methods) {
            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     right       wrong
                Method wrongMethod=wrongClazz.getDeclaredMethod(methodName, rightMethod.getParameterTypes());
                replace(wrongMethod,rightMethod);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
//调用到Native层,进行ArtMethod结构体的修改
private native static void replace(Method wrongMethod,Method rightMethod);
  1. 赋值android源码中art_method.h,只保留部分声明即可。
extern "C"
JNIEXPORT void JNICALL
Java_com_dongnao_andfix_DexManager_replace(JNIEnv *env, jobject instance, jint sdk,jobject wrongMethod,
                                           jobject rightMethod) {

//        ArtMethod  ----->

    art::mirror::ArtMethod *wrong= reinterpret_cast<art::mirror::ArtMethod *>(env->FromReflectedMethod(wrongMethod));
    art::mirror::ArtMethod *right= reinterpret_cast<art::mirror::ArtMethod *>(env->FromReflectedMethod(rightMethod));

//    wrong=right;
    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->dex_method_index_ = right->dex_method_index_;
    wrong->method_index_ = right->method_index_;
}

它的思想是修改错误ArtMethod结构体中方法入口和字节码地址等,为正确ArtMethod的结构体中对应的内容。这样堆内存对象指向方法时,会去方法区找到ArtMethod,因为ArtMethod的字节码地址修改了,往栈内存压栈的栈帧就会是正确的字节码内容,从而bug得到修复。

  • 实际开发中,修复bug不需要特意在修复的方法上添加注解。Andfix提供了差分包制作工具,能够比对修改前后的apk,找出修改了的方法块,再生成一个dex,使用jadx查看可以发现,dex里面有一个该工具创建的class类,包含了修改了的方法,方法上有一个类似Replace 的注解。

Tinker

tinker利用Java层的ClassLoader机制,将修复包中的dex插入到系统ClassLoader的dexElements前端,系统取到了我们替换的class。修复的粒度是整个类替换。
原理和动态加载apk一样。
参考:https://www.jianshu.com/p/fd9ed8b720ef

Andfix和Tinker的差异

由于执行方法时虚拟机层都要读取方法表然后组装栈帧压栈,Andfix可以实现实时热修复。Tinker利用的是ClassLoader机制,但是当一个类已经被加载后,ClassLoader会将其缓存在内存中,不会再去读取dexElement了,所以Tinker不能实现实时热修复。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容