美文网首页Android开发经验谈Android技术知识Android开发
史上超详细的AndFix热修复原理以及使用

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

作者: 凌烟醉卧 | 来源:发表于2019-09-25 19:26 被阅读0次

    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的一个缺点。

    相关文章

      网友评论

        本文标题:史上超详细的AndFix热修复原理以及使用

        本文链接:https://www.haomeiwen.com/subject/bafmyctx.html