美文网首页初见
模仿手写阿里andfix的实现原理

模仿手写阿里andfix的实现原理

作者: JasonChen8888 | 来源:发表于2020-06-16 20:10 被阅读0次

    正经前言

    当公司的项目出现问题了,早期的老套路子是解决bug,重新发新版本apk,但是随着技术不断的更新,线上项目出现严重问题,可以通过进行热修复,在不需要发布新版本的情形下进行问题处理。常见的热修复:阿里家的andfix和sophix, 腾讯家的tinker和QQ空间补丁技术...等等。

    个人用过两款热修复:andfix和tinker
    andfix和tinker区别:

    框架 优点 缺点
    andfix 不要重启app可以直接生效 存在兼容性问题
    tinker 没有兼容性问题 需要重启app

    今天主要分析一下Andfix,手写模仿Andfix的修复原理。

    开始正经撸码.jpeg

    开始正经撸码

    热修复是基于dex分包方案和Android虚拟机的类加载器(ClassLoader)实现的。

    • 实现思路
    1. 发现bug 并修改bug,将修复的java文件 编译成class 然后打包成dex 放到服务器 供客户端下载
    2. 将修复的方法体 Method 从dex 文件取出,将会出现bug的方法 Method 也取出来
    3. 将取出的正确的 和 错误的method 一并传到底层做替换操作
    4. 在底层进行替换

    原理

    andfix的原理就是通过dex的类进行替换修改存在的问题;
    热修复是基于类的层面:


    Andfix的原理.png

    dex多分包

    实现代码,打包生产dex文件

    • 栗子:以除数是0的异常,作为栗子

    bug类代码:

    package com.jason.andfix;
    
    public class Calculator {
    
      public int calculate() {
    
        int j = 10;
        int i = 0;
        int result = j / i;
        return result;
    
      }
    }
    

    修复的类代码:

    package com.jason.andfix.web;
    
    import com.jason.andfix.MethodReplace;
    
    public class Calculator {
    
      @MethodReplace(clazz = "com.jason.andfix.Calculator", method = "calculate")
      public int calculate() {
    
        int j = 10;
        int i = 1;
        int result = j / i;
        return result;
        
      }
    }
    

    上面两个类,一个bug类是com.jason.andfix.Calculator,一个修复类是com.jason.andfix.web.Calculator
    我们需要将修复的类打包成一个dex文件
    这边采用的是SDK默认的dx.bat的工具进行打包


    dx.bat在SDK所在的位置.png
    • 打包命令
    dx  --dex --output  生产的dex文件名  所要打包的类
    

    打包成功如下图,会在对应的目录下找到生成的out.dex文件,通常是会放到服务端,提供下载,这边demo上是直接将dex文件放到外置卡,省略了dex文件下载的过程


    dx命令将class打包成dex包.png

    Android的虚拟机

    • 基本虚拟机介绍
      Android jvm虚拟机采用的是JIT技术,字节码都需要通过即时编译器(just in time ,JIT)转换为机器码。
      Android虚拟机分为dalvik虚拟机和art(Android Runtime)虚拟机
      Dalvik 是 Android 4.4 之前的标准虚拟机,Art是Android4.4之后的标准虚拟机,在Android4.4到Android7.0之前dalvik和art虚拟机是同时存在的,只是在Android5.0开始,Android的app都是依赖于art虚拟机上运行。
      关于dalvik和art的详细介绍https://blog.csdn.net/u011330638/article/details/82830027

    • 二者的区别:
      Dalvik虚拟机在jit编译器是在app运行时发生的,所以在Android5.0以下的机器,运行时候通常会容易卡顿
      Art虚拟机是将jit的字节码转机器码的过程,放在了apk在安装的过程中,所以在Android5.0以及以上的系统上安装过程比较长,但是大大提高了app的运行效率,采用了空间换时间的策略。

    • 示例上的实现
      上面介绍了两种虚拟机,说明Android虚拟机的类加载器(ClassLoader)至少有两套;因此,我们需要针对这两个进行适配。

      虚拟机jvm.jpg
      因此,我们需要从系统源码入手,进行分析
      系统源码:从Android1.6到android8.1的各个版本的系统源码
      链接:https://pan.baidu.com/s/1i3tiGwpeDuDL955RDhNJ0A 提取码:yzyr

    jvm的类加载

    Java类的加载分为三个过程 :加载(load),连接(link),初始化(init);
    加载过程如下图:


    类加载.png

    类的对象结构图:


    ClassObject.png

    基于Dalvik虚拟机的实现修复

    dalvik虚拟机的源码(由于没有4.4以下的机器,所以采用了4.4的系统源码,后面的dalvik修复也是基于该版本)


    Android4.4.2系统源码dalvik虚拟机源码的头文件.png

    dalvik虚拟机的api入口文件是Dalvik.h,我们在开发项目需要用到底层的api,就需要手动进行引入,但是系统的源码都头文件的引入:

    • 整理前的系统源码Dalvik.h代码:
    #ifndef DALVIK_DALVIK_H_
    #define DALVIK_DALVIK_H_
    
    #include "Common.h"
    #include "Inlines.h"
    #include "Misc.h"
    #include "Bits.h"
    #include "BitVector.h"
    #include "libdex/SysUtil.h"
    #include "libdex/DexDebugInfo.h"
    #include "libdex/DexFile.h"
    #include "libdex/DexProto.h"
    #include "libdex/DexUtf.h"
    #include "libdex/ZipArchive.h"
    #include "DvmDex.h"
    #include "RawDexFile.h"
    #include "Sync.h"
    #include "oo/Object.h"
    #include "Native.h"
    #include "native/InternalNative.h"
    
    #include "DalvikVersion.h"
    #include "Debugger.h"
    #include "Profile.h"
    #include "UtfString.h"
    #include "Intern.h"
    #include "ReferenceTable.h"
    #include "IndirectRefTable.h"
    #include "AtomicCache.h"
    #include "Thread.h"
    #include "Ddm.h"
    #include "Hash.h"
    #include "interp/Stack.h"
    #include "oo/Class.h"
    #include "oo/Resolve.h"
    #include "oo/Array.h"
    #include "Exception.h"
    #include "alloc/Alloc.h"
    #include "alloc/CardTable.h"
    #include "alloc/HeapDebug.h"
    #include "alloc/WriteBarrier.h"
    #include "oo/AccessCheck.h"
    #include "JarFile.h"
    #include "jdwp/Jdwp.h"
    #include "SignalCatcher.h"
    #include "StdioConverter.h"
    #include "JniInternal.h"
    #include "LinearAlloc.h"
    #include "analysis/DexVerify.h"
    #include "analysis/DexPrepare.h"
    #include "analysis/RegisterMap.h"
    #include "Init.h"
    #include "libdex/DexOpcodes.h"
    #include "libdex/InstrUtils.h"
    #include "AllocTracker.h"
    #include "PointerSet.h"
    #if defined(WITH_JIT)
    #include "compiler/Compiler.h"
    #endif
    #include "Globals.h"
    #include "reflect/Reflect.h"
    #include "oo/TypeCheck.h"
    #include "Atomic.h"
    #include "interp/Interp.h"
    #include "InlineNative.h"
    #include "oo/ObjectInlines.h"
    
    #endif  // DALVIK_DALVIK_H_
    

    整理后的Dalvik.h代码:就是把我们需要用到的api手动copy到我们项目新建的Dalvik.h文件中

    //
    // Created by PC-3046 on 2020/6/16.
    //
    
    #ifndef ANDFIX_DALVIK_H
    #define ANDFIX_DALVIK_H
    
    #include <jni.h>
    #include <string.h>
    #include <stdio.h>
    #include <fcntl.h>
    #include <dlfcn.h>
    
    #include <stdint.h>    /* C99 */
    
    
    typedef uint8_t u1;
    typedef uint16_t u2;
    typedef uint32_t u4;
    typedef uint64_t u8;
    typedef int8_t s1;
    typedef int16_t s2;
    typedef int32_t s4;
    typedef int64_t s8;
    
    /*
     * access flags and masks; the "standard" ones are all <= 0x4000
     *
     * Note: There are related declarations in vm/oo/Object.h in the ClassFlags
     * enum.
     */
    enum {
        ACC_PUBLIC = 0x00000001,       // class, field, method, ic
        ACC_PRIVATE = 0x00000002,       // field, method, ic
        ACC_PROTECTED = 0x00000004,       // field, method, ic
        ACC_STATIC = 0x00000008,       // field, method, ic
        ACC_FINAL = 0x00000010,       // class, field, method, ic
        ACC_SYNCHRONIZED = 0x00000020,       // method (only allowed on natives)
        ACC_SUPER = 0x00000020,       // class (not used in Dalvik)
        ACC_VOLATILE = 0x00000040,       // field
        ACC_BRIDGE = 0x00000040,       // method (1.5)
        ACC_TRANSIENT = 0x00000080,       // field
        ACC_VARARGS = 0x00000080,       // method (1.5)
        ACC_NATIVE = 0x00000100,       // method
        ACC_INTERFACE = 0x00000200,       // class, ic
        ACC_ABSTRACT = 0x00000400,       // class, method, ic
        ACC_STRICT = 0x00000800,       // method
        ACC_SYNTHETIC = 0x00001000,       // field, method, ic
        ACC_ANNOTATION = 0x00002000,       // class, ic (1.5)
        ACC_ENUM = 0x00004000,       // class, field, ic (1.5)
        ACC_CONSTRUCTOR = 0x00010000,       // method (Dalvik only)
        ACC_DECLARED_SYNCHRONIZED = 0x00020000,       // method (Dalvik only)
        ACC_CLASS_MASK = (ACC_PUBLIC | ACC_FINAL | ACC_INTERFACE | ACC_ABSTRACT
                          | ACC_SYNTHETIC | ACC_ANNOTATION | ACC_ENUM),
        ACC_INNER_CLASS_MASK = (ACC_CLASS_MASK | ACC_PRIVATE | ACC_PROTECTED
                                | ACC_STATIC),
        ACC_FIELD_MASK = (ACC_PUBLIC | ACC_PRIVATE | ACC_PROTECTED | ACC_STATIC
                          | ACC_FINAL | ACC_VOLATILE | ACC_TRANSIENT | ACC_SYNTHETIC
                          | ACC_ENUM),
        ACC_METHOD_MASK = (ACC_PUBLIC | ACC_PRIVATE | ACC_PROTECTED | ACC_STATIC
                           | ACC_FINAL | ACC_SYNCHRONIZED | ACC_BRIDGE | ACC_VARARGS
                           | ACC_NATIVE | ACC_ABSTRACT | ACC_STRICT | ACC_SYNTHETIC
                           | ACC_CONSTRUCTOR | ACC_DECLARED_SYNCHRONIZED),
    };
    
    typedef struct DexProto {
        u4* dexFile; /* file the idx refers to */
        u4 protoIdx; /* index into proto_ids table of dexFile */
    } DexProto;
     ................................ 代码太长,省略,后续会提供完整的项目下载 ....................
    
    • 撸码实现
    1. Java实现dex文件的加载
      DexFileManager.java:
    package com.jason.andfix;
    
    import android.content.Context;
    import android.os.Build;
    
    import java.io.File;
    import java.io.IOException;
    import java.lang.reflect.Method;
    import java.util.Enumeration;
    
    import dalvik.system.DexFile;
    
    public class DexFileManager {
    
      private Context context;
    
      private static final DexFileManager INSTANCE = new DexFileManager();
    
      private DexFileManager(){}
    
      public static DexFileManager getInstance() {
        return INSTANCE;
      }
    
      public void setContext(Context context) {
        this.context = context.getApplicationContext();
      }
    
      /**
       * 加载dex文件
       * @param path
       */
      public void loadDexFile(String path) {
        File file = new File(path);
        loadDexFile(file);
      }
    
      public void loadDexFile(File file) {
        try {
          //dalvik虚拟机的dex对象
          DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),
              new File(context.getCacheDir(), "opt").getAbsolutePath(), Context.MODE_PRIVATE);
          //下一步  得到class   ----取出修复好的Method
          Enumeration<String> entry= dexFile.entries();
          while (entry.hasMoreElements()) {
            //拿到全类名
            String className=entry.nextElement();
            //Class.forName(className);   拿到修复的dex的类
            Class clazz = dexFile.loadClass(className, context.getClassLoader());
            if (clazz != null) {
              fixClazz(clazz);
            }
    
          }
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    
      private void fixClazz(Class fixClazz) {
        //修复好的class
        Method[] methods = fixClazz.getDeclaredMethods();
        for (Method rightMethod : methods) {
          MethodReplace replace = rightMethod.getAnnotation(MethodReplace.class);
          if(replace == null) {
            continue;
          }
    
          String wrongClazzName = replace.clazz();
          String wrongMethodName = replace.method();
    
          try{
            Class clazz = Class.forName(wrongClazzName);
            Method wrongMethod = clazz.getDeclaredMethod(wrongMethodName, rightMethod.getParameterTypes());
            if (Build.VERSION.SDK_INT <= 19) {  //实际是<=18 ,由于没有4.4以下的机器,改成了19进行测试
              replaceDalvik(Build.VERSION.SDK_INT ,wrongMethod, rightMethod);
            }else {
              replaceArt(wrongMethod, rightMethod);
            }
          } catch (ClassNotFoundException e) {
            e.printStackTrace();
          } catch (NoSuchMethodException e) {
            e.printStackTrace();
          }
        }
      }
    
    
     //修复在通过jni调用底层进行method替换
      private native  void replaceArt(Method wrongMethod, Method rightMethod);
      public native void replaceDalvik(int sdk, Method wrongMethod, Method rightMethod);
    }
    

    上面实现了dex的文件加载,然后将加载到的dex解析,获取到我们修复好的类,再通过jni调用dalvik的C++底层进行底层method替换。关于JNI忘记的同学,可以参考我之前写的https://www.jianshu.com/p/3fdf924680af

    1. MethodReplace注解
      该注解是用来标识修复的方法,以及被修复的方法和类名
    package com.jason.andfix;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface MethodReplace {
      String  clazz();
      String  method();
    }
    
    1. Dalvik虚拟机api的jni的实现method替换
    extern "C"
    JNIEXPORT void JNICALL
    Java_com_jason_andfix_DexFileManager_replaceDalvik(JNIEnv *env, jobject thiz, jint sdk,
                                                       jobject wrong_method, jobject right_method) {
    
        Method *wrong = (Method *) env->FromReflectedMethod(wrong_method);
        Method *right =(Method *) env->FromReflectedMethod(right_method);
    
        //ClassObject
        void *dvm_hand=dlopen("libdvm.so", RTLD_NOW);
        //sdk  10    以前是这样   10会发生变化
        findObject= (FindObject) dlsym(dvm_hand, sdk > 10 ?
                                                 "_Z20dvmDecodeIndirectRefP6ThreadP8_jobject" :
                                                 "dvmDecodeIndirectRef");
        findThread = (FindThread) dlsym(dvm_hand, sdk > 10 ? "_Z13dvmThreadSelfv" : "dvmThreadSelf");
        // method   所声明的Class
    
        jclass methodClaz = env->FindClass("java/lang/reflect/Method");
        jmethodID rightMethodId = env->GetMethodID(methodClaz, "getDeclaringClass",
                                                   "()Ljava/lang/Class;");
        //dalvik  odex   机器码
        //  firstFiled->status=CLASS_INITIALIZED
        //  art不需要    dalvik适配
        jobject ndkObject = env->CallObjectMethod(right_method, rightMethodId);
        ClassObject *firstFiled = (ClassObject *) findObject(findThread(), ndkObject);
        firstFiled->status=CLASS_INITIALIZED;
        wrong->accessFlags |= ACC_PUBLIC;
    
        wrong->methodIndex=right->methodIndex;
        wrong->jniArgInfo=right->jniArgInfo;
        wrong->registersSize=right->registersSize;
        wrong->outsSize=right->outsSize;
    //    方法参数 原型
        wrong->prototype=right->prototype;
    //
        wrong->insns=right->insns;
        wrong->nativeFunc=right->nativeFunc;
    }
    
    1. 测试运行(在Android4.4的机器运行),将我们最开始生产的out.dex放到手机的外置存储卡;


      Android4.4手机上的修复结果.png

    基于Art虚拟机上实现热修复

    art虚拟机的源码(由于没有5.1以下的机器,所以采用了5.1的系统源码,后面的art修复也是基于该版本)


    Android5.1系统的源码art虚拟机的代码头文件.png

    整理后的art_method.h在后续源码中

    • Art虚拟机api的jni的实现method替换
    extern "C"
    JNIEXPORT void JNICALL
    Java_com_jason_andfix_DexFileManager_replaceArt(JNIEnv *env, jobject thiz, jobject wrong_method,
                                                    jobject right_method) {
    //    art虚拟机替换  art  ArtMethod  ---》Java方法
        art::mirror::ArtMethod *wrong = (art::mirror::ArtMethod *) env->FromReflectedMethod(wrong_method);
        art::mirror::ArtMethod *right = (art::mirror::ArtMethod *) env->FromReflectedMethod(right_method);
    
        wrong->declaring_class_=right->declaring_class_;
    
        wrong->dex_code_item_offset_=right->dex_code_item_offset_;
        wrong->method_index_=right->method_index_;
        wrong->dex_method_index_=right->dex_method_index_;
    
    
        //入口
        wrong->ptr_sized_fields_.entry_point_from_jni_=right->ptr_sized_fields_.entry_point_from_jni_;
        //    机器码模式
        wrong->ptr_sized_fields_.entry_point_from_quick_compiled_code_=right->ptr_sized_fields_.entry_point_from_quick_compiled_code_;
    }
    
    • 测试运行的效果(这边在6.0的机器上进行测试,需要动态申请存储权限),同样将out.dex 放到手机外置卡


      Android6.0机器上的允许结果.png

    总结

    不管是art虚拟机还是dalvik虚拟机,实现热修复的关键是,在底层进行method的指针的替换,将错误的method的指针替换到修复后的新的method的指针。

    结语

    关于目前文章描述的是dalvik和art的适配,但是在Android7.0的系统,Google又进行了新的调整,因此art的热修复需要再7.0的系统在做一次兼容处理。
    以上就是说模拟手写阿里Andfix的内容,如有错误,欢迎指正。

    参考文献

    https://www.jianshu.com/p/cc66138d72b1
    https://blog.csdn.net/u011330638/article/details/82830027

    我的博客将同步至腾讯云+社区

    https://cloud.tencent.com/developer/column/87704

    相关文章

      网友评论

        本文标题:模仿手写阿里andfix的实现原理

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