史上超详细的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 

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 
#include 
#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(env->FromReflectedMethod(wrongMethod));
    art::mirror::ArtMethod *right= reinterpret_cast(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
#include
#include

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  :新apk
-t  :旧apk
-o  :输出目录(补丁文件的存放目录) 
-k : 打包所用的keystore 
-p : keystore的密码 
-a : keystore 用户别名 
-e : 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 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(env->FromReflectedMethod(wrongMethod));
    art::mirror::ArtMethod *right= reinterpret_cast(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的一个缺点。

你可能感兴趣的:(史上超详细的AndFix热修复原理以及使用)