史上超详细的AndFix热修复原理以及使用

AndFix使用范围
修复紧急或者比较小的bug。

AndFix最大优势:及时生效,不需要重启

及时生效的原因
通过native调用:
未下载修复包加载一次
下载修复包后加载一次,下载完成后调用

缺点
稳定性较差,会受到国内ROM厂商对ArtMethod结构更改的影响,如果要适配的话,是很麻烦的。

实现步骤
1.在要修改的方法上添加注解并生成补丁包(.apatch),其实就是一个dex文件。
2.获取补丁包中的补丁类并遍历其中的方法获取待注解的方法
3.使用补丁中的方法替换bug中的方法

AndFix实现原理
需要把java当做一个xml,不然很难理解AndFix的实现原理,首先需要了解java方法里面崩溃和java方法怎样被修复。

先来看看java方法到底如何执行的,Android是怎样加载java类的(java方法在虚拟机是怎么执行的)?

Android程序执行的第一个类是 ZygoteInit.java,当这个类加载的时候,虚拟机开启一个Zygote进程,为Zygote进程分配一个内存空间,会分配5大区,PC计数器,本地方法栈,方法区,堆区,栈区,主要是后3个,当用户点击了APP,Zygote会以命令行的方式启动一个APP,APP最先加载到内存的类是Application.java,系统如果加载,也会加载Application.class,通过的是ClassLoader来加载的,当一个类被加载到方法区时,会在方法区开辟一个方法表,方法表的大小是由类中的方法多少来决定的,可以把方发表理解成一个数组。new的时候在方法区加载方发表的时候同时会在堆区开辟空间,堆区存放的是类的成员变量(如存放的application1),所有进程都是Zygote进程来孵化的。

先来写一段伪代码:

Application application = new Application();
application.onCreate();

当声明一个类的时候,如

Test test;

这时不会把这个Test加载到内存中,只会在方法区定义一个符号变量叫Test
int(Test 符号变量)。

打断点的时候会看到对象中有 kclass,kclass就是堆中开辟的空间指向的符号变量,同时这个符号变量指向了方法表,当onCreate()方法被调用的时候,由堆区的对象发送一个事件给符号变量,符号变量一看是调用某个方法,此时会将方发表中的onCreate结构体(onCreate是个方法,其实它是个结构体)中对应的字节码进行压栈(压栈是在栈区执行的操作),栈的特点是先进后出,压成一个栈帧,转变成汇编语言。

onCreate结构体
ArtMethod{
    方法入口
    字节码地址
}

那什么时候才会加载这个Test类呢?
只有在new的时候或者反射的时候才会加载类到内存。

 new Test()

了解了这些后,然后需要了解方法存放在 .class中,而.class 存放在dex中

安卓虚拟机加载的dex文件,所以实际开发中热修复是从网络下载一个dex文件来操作。

Java中的Method和虚拟机的ArtMethod是一一对应的,可以通过java中的Method找到虚拟机中的ArtMethod。

art_method.h文件

#include <stdint.h>

namespace art{
    namespace mirror{
        class Object{
          
            uint32_t klass_;
           
            uint32_t monitor_;

        };
        class ArtMethod:public Object{
        public:
           
            uint32_t access_flags_;
            uint32_t dex_code_item_offset_;
            uint32_t dex_method_index_;
            uint32_t method_index_; 
            uint32_t dex_cache_resolved_methods_;
            uint32_t dex_cache_resolved_types_;
            uint32_t declaring_class_;
        };
    }
}

native-lib.cpp文件

#include <jni.h>
#include <string>
#include "art_method.h"


extern "C"
JNIEXPORT void JNICALL
Java_com_dongnao_andfix_DexManager_replace(JNIEnv *env, jobject instance, jobject wrongMethod,
                                           jobject rightMethod) {
    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->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_;
}

上面这段代码在5.0,6.0是没有问题的,如果在其它的版本运行可能会有问题。

Java中的Method和虚拟机的ArtMethod是一一对应的,可以通过java中的Method找到虚拟机中的ArtMethod。

我们是如何加载一个类的?
java中是通过 new 反射 classload 来加载类的。new 反射 classload最终都是通过JNI中的FindClass来加载类的。
native是通多FindClass来加载类的,FindCalss最终会进入class_linker.cc(6.0源码)这个类。

FindClass这个类的作用:
1.检查一个是否正确
2.检查并做加载类的准备工作

cliss_linker.cc中有个DefineClass,DefineClass定义一个空的类,相当于画板的作用(JavaBean),java中的每个类在虚拟机中都对应一个相应的结构体,然后通过newHandler的方式来创建kclass。通过LoadClass来加载一个类,此时Class还在dex中,从dex中加载class,所以需要将Class的信息给到空的类来构建成员变量表(ArtField)和方法表(ArtMethod),其中对成员变量和方法做了判断,不为0的情况下赋值给kclass。
java方法通过虚拟机加载到内存中,虚拟机给每个方法分配的内存字节数是固定的,字节数是根据当前的手机型号或者是Android版本不同而不同。

虚拟机分为Dalvik虚拟机和Art虚拟机:
Dalvik使用的jit(即时编译技术),虚拟机会加载libdalvik.so这个库。
Art使用的是AOT预编译技术,4.4后出现了Art,虚拟机会加载libart.so这个库

Dalvik虚拟机模仿的是Java虚拟机(JVM),它通过libdalvik.so来进行类的加载。
dex到class经历4个阶段 :
构建-初始过程-赋值阶段-初始化完成阶段
如果初始化没有完成,这个方法是不能调用的。

art
字节码二进制文件 ——>本地机器执行指令,所以速度很快

需要注意的是,art和dalvik虚拟机加载类的方式不同,art中使用的是kclass,而dalvik使用的是ClassObject。

因为我们知道,加载.so文件是需要相关的依赖库的,那如果没有相关的依赖库时,虚拟机时如何加载.so文件的呢?

通过一个小例子来说明对虚拟机的hook:
Cat.c文件

int add(int a,int b){
    return (a*b);
}

main.c文件

#include<stdio.h>
#include<stdlib.h>
#include<dlfcn.h>

typedef int (*ADD)(int,int);
int main(){
    void *handle=dlopen("./libdavik.so",RTLD_LAZY);
    ADD add=NULL;
    *(void **)(&add)=dlsym(handle,"add");
    int reslut=add(2,5);
    printf("%d\n",reslut);
    return 0;       

}

通过执行命令:

root@iZbp15ohd7pim2jay3vbwyZ:~# gcc -fPIC -shared Cat.c -o libdavik.so
root@iZbp15ohd7pim2jay3vbwyZ:~# ls
Cat.c  libdavik.so  main.c
root@iZbp15ohd7pim2jay3vbwyZ:~# gcc -o main main.c -ldl
root@iZbp15ohd7pim2jay3vbwyZ:~# ls
Cat.c  libdavik.so  main  main.c
root@iZbp15ohd7pim2jay3vbwyZ:~# ./main 
10

可以看到,通过将 Cat.c 编译为 libdavik.so,然后将main.c编译为可执行程序,main可以看作我们的虚拟机,让它来加载libdavik.so这个库,通过的是如下这个重要的方法:

void *handle=dlopen("./libdavik.so",RTLD_LAZY);

总结:
1.每一个java方法的大小是固定的
2.方法标中的方法与方法之间是紧密联合的(可以理解为一块连续的内存)

下面来看看阿里的AndFix如何使用
在app下的build.gradle添加依赖库:
implementation 'com.alipay.euler:andfix:0.4.0@aar'

创建一个Caclutor.java,来演示异常以及修复后的情况

public class Caclutor {
    public void test(Context context){
//        throw new RuntimeException("出异常了");
        Toast.makeText(context,"修复了",Toast.LENGTH_SHORT).show();
    }
}

然后再MainActivity.java中来操作AndFix的相关API

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void test(View view) {
        Caclutor caclutor = new Caclutor();
        caclutor.test(this);
    }

    public void fix(View view) {
        PatchManager patchManager = new PatchManager(this);
        try{
            /**
             * 这段代码应该写在application中
             */
            String versionName = getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
            //初始化
            patchManager.init(versionName);
            /**
             * loadPatch() 从网络上下载修复包放到AndFix的私有目录中,然后去加载所有已经存在的修复包
             */
            patchManager.loadPatch();
            File file = new File(Environment.getExternalStorageDirectory(),"out.apatch");
            patchManager.addPatch(file.getAbsolutePath());
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

分别打新旧两个包,old.apk和new.apk,针对Caclutor修改前后。

从git上下载阿里的开源库 https://github.com/alibaba/AndFix

找到tools文件夹,在windows下使用 apkpatch.bat这个工具,在Linux下使用apkpatch.sh这个工具。



old.apk和new.apk是我打包后的两个apk,放到里面的。

命令行定位到tools文件夹,需要使用apkpatch.bat(我的系统是windows系统),执行命令:

apkpatch.bat -f new.apk -t old.apk -o output -k key.jks -p 123456 -a test -e  123456

参数说明

apkpatch.bat -f 新apk -t 旧apk -o 输出目录 -k app签名文件 -p 签名文件密码 -a 签名文件别名 -e 别名密码

-f <new.apk> :新apk
-t <old.apk> :旧apk
-o <output> :输出目录(补丁文件的存放目录) 
-k <keystore>: 打包所用的keystore 
-p <password>: keystore的密码 
-a <alias>: keystore 用户别名 
-e <alias password>: keystore 用户别名密码

命令完成后会在当前目录下生成一个output文件夹说明成功了


output文件夹里面有一个名字很长后缀以apttch结尾的文件就是我们所需要的文件,给名为 out.apatch放到外部储存中。


难道这个apatch文件就是我们所需要的吗,前面不是说安卓虚拟机加载的是dex文件吗?
其实apatch是阿里的命名规则,它的本质就是个压缩包,我们把apatch改为zip,然后打开



看到没有,这个 classes.dex 就是我们所需要的虚拟机要加载的修复后的文件,所以不要被这个apatch文件所迷惑了。

那AndFix是如何知道某个方法是需要修复的呢,其实这个很简单,就是将一行一行的代码进行比较,然后把改变的方法加上注解

package com.example.myapplication;

import android.content.Context;

public class Caclutor {
    @Replace(clazz = "com.example.myapplication2.Caclutor",method = "test")//注意:这里com.example.myapplication2.Caclutor这个全类名,和本类的不一样,这个包是修复后的Caclutor
    public void test(Context context) {
        //throw new RuntimeException("出异常了");
    }
}

Replace.java

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Replace {
    String clazz();
    String method();
}
public void load(File file) {
        try {
            DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),
                    new File(context.getCacheDir(), "opt").getAbsolutePath(),
                    Context.MODE_PRIVATE);
            Enumeration<String> entry= dexFile.entries();
            while (entry.hasMoreElements()) {
//                全类名
                String className = entry.nextElement();
                Class realClazz=dexFile.loadClass(className, context.getClassLoader());
                if (realClazz != null) {
                    fixClass(realClazz);
                }
//                Class.forName(className);//forName
            }
        } catch (Exception e) {
            e.printStackTrace();
        }


    }

    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中实现

extern "C"
JNIEXPORT void JNICALL
Java_com_dongnao_andfix_DexManager_replace(JNIEnv *env, jobject instance, 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_;
}

上面这个是art虚拟机下的,而如果是dalvik虚拟机则不是所示,并且5.0之上的各个版本的ArtMethod这个结构体都会不同,所以适配起来很麻烦,这也是AndFix的一个缺点。

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

推荐阅读更多精彩内容

  • 读《诗经》前几篇都是爱情如何美好,男子女子们如何勇敢大胆追求爱情,一直抱着宁可信其有不可信其无的心态欣赏爱的美好,...
    大脸猫的自留地阅读 1,095评论 0 1
  • 【关系问答】接纳自己的对立面 问答:拥抱完整的自己 1.问:之前文章里说过,人是过去经历的总和,如果有人和你有...
    o鹿鸣阅读 505评论 0 0
  • 笔架案,镇尺印章纸砚。 泼墨挥毫方寸间,闲暇把玩练。 毛瑟精兵百万,运用自如艰难。 功力终浅不遗憾,当红颜作伴。
    鱼在海阅读 385评论 1 5
  • 都说孩子是母亲身上掉下来的一块肉,很难想象是什么原因让一个母亲杀死自己八个月的孩子。 最近被安利一部只有六集的日剧...
    夏默念笙阅读 1,020评论 0 1
  • 上班的地方相对来说比较自由,每天到了下午3-4点左右我就要开始吃零食。 今天早上上班我在路边的超市买了一袋果味的夹...
    狐狸家的慢生活阅读 182评论 0 4