(十一)Android AndFix 热修复原理

版权声明:本文为博主原创文章,未经博主允许不得转载。
本文纯个人学习笔记,由于水平有限,难免有所出错,有发现的可以交流一下。

一、so 的插件化

在进行 Android 的热修复讲解之前,先大概讲一下 so 的插件化,这在热修复中需要用到。

一个 so 可以通过路径去加载另外一个 so,从而实现一个 so 库调用另外一个 so 库中的方法。

先来一个被调用的 so,这个so 里面只有一个 add 方法。

plugin.c:

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

在 linux 下使用命令 gcc -fPIC -shared plugin.c -o libplugin.so 进行编译成 so。

编译成so命令
-fPIC 压制警告
-shared 动态库
-o 生成目标的文件名

在同一目录下编写个 main.c 进行调用生成的 so 库。

main.c

#include<stdio.h>

#include<stdlib.h>

#include<dlfcn.h>

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

}

在 linux 下使用命令 gcc -rdynamic -o main main.c -ldl 编译成可执行文件。使用命令 ./main 进行执行操作。

主要是使用了 dlfcn.h 下的方法进行加载,这样修改 plugin.c 文件后,重新编译,不需要对 main 进行任何修改,就可以直接改变结果。对于安卓来说,采用插件化,当更新 so 库的时候,不需要进行整个应用的更新。

二、虚拟机与热修复

java 虚拟机       加载 class

davlik 虚拟机     加载 dex  

art  虚拟机       加载 dex 

安卓虚拟机分为 davlik 虚拟机和 art 虚拟机。davlik 虚拟机是 Android 18 以下的系统,art 虚拟机是 Android 18 以上的系统 ,Android 19、Android 20 同时支持两个虚拟机,Android 21 开始就不在维护 davlik 虚拟机。这两种虚拟机采用的修复方式不同。

安卓虚拟机没有采用 jvm 的虚拟机,主要是因为版权、手机性能等原因,但是安卓的虚拟机支持了所有的 jvm虚拟机的接口。jvm 虚拟机是给予栈进行运行的,而 davlik 虚拟机基于寄存器进行运行,速度更快。

上面提到,davlik 虚拟机切换到 art 虚拟机是在 Android 19、Android 20 有空档期,这时候手机是同事支持两个虚拟机的,默认是 davik 虚拟机,可以在手机中 设置–》开发者 –》即时编译 进行设置切换为 art 虚拟机。

虚拟机区别:对于手机来说,最终运行的都是 class字节码 。dalvik 采用的是 JIT 技术,字节码都需要通过即时编译器(just in time ,JIT)转换为机器码,在进行运行。发生时间是在方法运行时,如果摸个方法频繁被调用,则会缓存下来,进行一定的性能优化。即使这样,即使编译还是限制了应用的性能,导致卡顿。
art 虚拟机在安装 apk 的时候相比 dalvik 虚拟机需要花费更多的时间。dalvik 虚拟机是先把 dex 文件加载为 odex,进行一定的优化,再进行使用。art 是把 dex 文件,使用 Android 系统自带的 dex2oat 工具转化成 OAT 文件,OAT 文件是一种 Android 私有 elf 文件格式,它不仅包含有从 dex 文件翻译而来的本地机器指令,还包含有原来的 dex文件内容。安装耗时主要是在这里的转换,但是这样的话,art 虚拟机可以直接运行 OAT 文件下的机器指令,而不需要进行转换。

热修复的原理大部分都来自于安卓App热补丁动态修复技术介绍

强烈建议先了解一下。

三、davlik 修复手写实现

1.修复流程

1.发现 bug,并修改 bug,将修复的 java 文件编译成 class 文件,然后打包成 dex,放到服务器供客户端下载。(发现bug 可以使用 CrashHandler implements UncaughtExceptionHandler 进行全局捕获,上送)

2.将修复的方法体 Method 从dex 文件取出,将会出现 bug 的方法 Method 也取出来。

3.将取出的正确的和错误的 method 一并传到底层做替换操作。

4.底层替换(应该是 指针替换)。

2.模拟异常

编写一个简单的安卓项目,认为制造一个除 0 异常,进行模拟生产上的应用 bug。

MainActivity :

public class MainActivity extends AppCompatActivity {
    static {
        System.loadLibrary("native-lib");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

    }

    public void jisuan(View view) {
        Caclutor caclutor=new Caclutor();
        caclutor.caculator();
    }

    public void fix(View view) {
    }
}

activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >
    <TextView
        android:id="@+id/result"
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:gravity="center"
        android:text="结果" />
    <TextView
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:gravity="center"
        android:text="计算 10/0"
        android:onClick="jisuan"
        />
    <TextView
        android:id="@+id/fix"
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:gravity="center"
        android:text="修复"
        android:onClick="fix"/>

</LinearLayout>

Caclutor :

public class Caclutor {

    public int caculator(){
        int i = 0;
        int j = 10;
        //模拟异常产生
        return j / i;
    }
}

点击界面上的计算按钮时候,会触发 Caclutor 类的 caculator()方法 除 0 异常。

3.修复

在服务器进行编写修复代码,然后变成成 class 文件,再打包成 dex 进行下载。

1.服务器代码

由于修复的时候需要指定具体要修复的类名与方法名,这边采用一个注解进行标注。
Replace :

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Replace {
    String  clazz();
    String  method();
}

对要修复的方法进行重新编写,使用上述注解标注全类名和方法名。
Caclutor :

public class Caclutor {

    @Replace(clazz = "com.xiaoyue.andfix.Caclutor",method = "caculator")
    public int caculator(){
        int i = 1;
        int j = 10;
        return j / i;
    }
}

2.编译成 class

这一步正常编译器都会自动帮我们进行生成,我们需要做的是把修复的 class 抽出来,记住要保留报的全路径。

这里写图片描述

在这里,修复类 Caclutor 的包名是 com.xiaoyue.andfix.web ,把修复类连带包名拷贝到 dex 文件夹下。

3.打包 dex

class 打包成 dex,需要用到 sdk 自带的工具,在 sdk 下 build-tools 随便一个版本下有 dx.bat 这个工具。
这里写图片描述

命令行窗口,切换到当前 dx.bat 所在的目录,然后执行 dx –dex –output 生产的dex文件 要打包的class 命令进行打包。
这里写图片描述

则在对应的目录下生成 out.dex 文件。
这里写图片描述

生成的 dex 文件需要自行确认机制进行下载,正常是在启动的时候进行检测,是否有需要更新的 dex,有的话进行下载更新。

4.解析 dex

客户端下载到需要进行修复的 dex 后,需要对 dex 进行解析。

Replace :

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Replace {
    String  clazz();
    String  method();
}

由于解析的时候需要通过注解确认到需要进行修复的具体类名和方法名,所以在客户端也是需要这个注解的。

DexManager :

public class DexManager {

    private Context context;

    public DexManager(Context context) {
        this.context = context;
    }

    public void loadFile(File file) {
        try {
            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);
                Class clazz=dexFile.loadClass(className, context.getClassLoader());
                if (clazz != null) {
                    fixClazz(clazz);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void fixClazz(Class realClazz) {
        //服务器修复好的 realClazz
        Method[] methods = realClazz.getDeclaredMethods();
        for (Method rightMethod : methods) {
            Replace replace = rightMethod.getAnnotation(Replace.class);
            if (replace == null) {
                continue;
            }
            //找到了修复好的 Method
            //找到出 bug 的 Method
            String wrongClazz = replace.clazz();
            String  wrongMethodName = replace.method();
            try {
                Class clazz=Class.forName(wrongClazz);
                //传的方法参数直接获取已修复方法的参数,从而找到对应的方法
                Method wrongMethod = clazz.getDeclaredMethod(wrongMethodName, rightMethod.getParameterTypes());
                if (Build.VERSION.SDK_INT <= 18) {
                    Log.e("DexManager", "replace begin");
                    replace(Build.VERSION.SDK_INT ,wrongMethod, rightMethod);
                }
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 新旧方法替换
     * @param wrongMethod
     * @param rightMethod
     */
    public native void replace(int SDKVersion, Method wrongMethod, Method rightMethod);

}

DexManager 是对 dex 修复包进行解析,修复 bug。先使用 DexFile 进行加载 dex,获取到修复的方法,同时通过该方法的注解获取到要修复的类名和方法名。获取到类名跟方法名,再通过反射获取到出 bug 的方法,然后调用底层的替换,对方法进行修复。

davlik 虚拟机使用于 Android 4.3 (API 18) 之前,在Android 5.0 (API 21)开始不再支持,替换为 art 虚拟机。这两个虚拟机对修复采用的替换方法不同,分开讲解。

5、davlik 虚拟机修复

native-lib.cpp:

#include <jni.h>
#include "dalvik.h"
#include<dlfcn.h>

typedef Object *(*FindObject)(void *thread, jobject jobject1);
typedef  void* (*FindThread)();
FindObject  findObject;
FindThread  findThread;

extern "C"

JNIEXPORT void JNICALL
Java_com_xiaoyue_andfix_DexManager_replace(
        JNIEnv *env, jobject instance, jint SDKVersion, jobject wrongMethod, jobject rightMethod) {

    //找到 java 方法在虚拟机中对应的 MeThod 结构体
    Method *wrong = (Method *)(env->FromReflectedMethod(wrongMethod));
    Method *right = (Method *)(env->FromReflectedMethod(rightMethod));

    //修复方法
    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;
}

这边使用到一个 dalvik.h 头文件,这个主要是引入 android-4.4.4_r1\dalvik\vm\oo\Object.h 下的 Method 结构体。DVM的源码位于 dalvik/ 目录下,其中 dalvik/vm 目录下的内容是 DVM 的具体实现部分,它会被编译成libdvm.so。

dalvik.h :

#include <string.h>
#include <jni.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;

typedef void (*DalvikBridgeFunc)(const u4* args, void* pResult,
        const void* method, void* self);

struct Field {
    void* clazz; /* class in which the field is declared */
    const char* name;
    const char* signature; /* e.g. "I", "[C", "Landroid/os/Debug;" */
    u4 accessFlags;
};

struct Method;
struct ClassObject;

typedef struct Object {
    /* ptr to class object */
    struct ClassObject* clazz;

    /*
     * 类的加载过程
     * A word containing either a "thin" lock or a "fat" monitor.  See
     * the comments in Sync.c for a description of its layout.
     */
    u4 lock;
} Object;

struct InitiatingLoaderList {
    /* a list of initiating loader Objects; grown and initialized on demand */
    void** initiatingLoaders;
    /* count of loaders in the above list */
    int initiatingLoaderCount;
};

enum PrimitiveType {
    PRIM_NOT = 0, /* value is a reference type, not a primitive type */
    PRIM_VOID = 1,
    PRIM_BOOLEAN = 2,
    PRIM_BYTE = 3,
    PRIM_SHORT = 4,
    PRIM_CHAR = 5,
    PRIM_INT = 6,
    PRIM_LONG = 7,
    PRIM_FLOAT = 8,
    PRIM_DOUBLE = 9,
}typedef PrimitiveType;

enum ClassStatus {
    CLASS_ERROR = -1,

    CLASS_NOTREADY = 0, CLASS_IDX = 1, /* loaded, DEX idx in super or ifaces */
    CLASS_LOADED = 2, /* DEX idx values resolved */
    CLASS_RESOLVED = 3, /* part of linking */
    CLASS_VERIFYING = 4, /* in the process of being verified */
    CLASS_VERIFIED = 5, /* logically part of linking; done pre-init */
    CLASS_INITIALIZING = 6, /* class init in progress */
    CLASS_INITIALIZED = 7, /* ready to go */
}typedef ClassStatus;

typedef struct ClassObject {
    struct Object o; // emulate C++ inheritance, Collin

    /* leave space for instance data; we could access fields directly if we
     freeze the definition of java/lang/Class */
    u4 instanceData[4];

    /* UTF-8 descriptor for the class; from constant pool, or on heap
     if generated ("[C") */
    const char* descriptor;
    char* descriptorAlloc;

    /* access flags; low 16 bits are defined by VM spec */
    u4 accessFlags;

    /* VM-unique class serial number, nonzero, set very early */
    u4 serialNumber;

    /* DexFile from which we came; needed to resolve constant pool entries */
    /* (will be NULL for VM-generated, e.g. arrays and primitive classes) */
    void* pDvmDex;

    /* state of class initialization */
    ClassStatus status;

    /* if class verify fails, we must return same error on subsequent tries */
    struct ClassObject* verifyErrorClass;

    /* threadId, used to check for recursive <clinit> invocation */
    u4 initThreadId;

    /*
     * Total object size; used when allocating storage on gc heap.  (For
     * interfaces and abstract classes this will be zero.)
     */
    size_t objectSize;

    /* arrays only: class object for base element, for instanceof/checkcast
     (for String[][][], this will be String) */
    struct ClassObject* elementClass;

    /* arrays only: number of dimensions, e.g. int[][] is 2 */
    int arrayDim;
    PrimitiveType primitiveType;

    /* superclass, or NULL if this is java.lang.Object */
    struct ClassObject* super;

    /* defining class loader, or NULL for the "bootstrap" system loader */
    struct Object* classLoader;

    struct InitiatingLoaderList initiatingLoaderList;

    /* array of interfaces this class implements directly */
    int interfaceCount;
    struct ClassObject** interfaces;

    /* static, private, and <init> methods */
    int directMethodCount;
    struct Method* directMethods;

    /* virtual methods defined in this class; invoked through vtable */
    int virtualMethodCount;
    struct Method* virtualMethods;

    /*
     * Virtual method table (vtable), for use by "invoke-virtual".  The
     * vtable from the superclass is copied in, and virtual methods from
     * our class either replace those from the super or are appended.
     */
    int vtableCount;
    struct Method** vtable;

} ClassObject;

typedef struct Method {
    struct ClassObject *clazz;
    u4 accessFlags;
//u2 methodIndex   方法表里面的索引
    u2 methodIndex;

    u2 registersSize; /* ins + locals */
    u2 outsSize;
    u2 insSize;

    /* method name, e.g. "<init>" or "eatLunch" */
    const char* name;

    /*
     * Method prototype descriptor string (return and argument types).
     *
     * TODO: This currently must specify the DexFile as well as the proto_ids
     * index, because generated Proxy classes don't have a DexFile.  We can
     * remove the DexFile* and reduce the size of this struct if we generate
     * a DEX for proxies.
     */
    DexProto prototype;

    /* short-form method descriptor string */
    const char* shorty;

    /*
     * The remaining items are not used for abstract or native methods.
     * (JNI is currently hijacking "insns" as a function pointer, set
     * after the first call.  For internal-native this stays null.)
     */

    /* the actual code */
    u2* insns;

    /* cached JNI argument and return-type hints */
    int jniArgInfo;

    /*
     * Native method ptr; could be actual function or a JNI bridge.  We
     * don't currently discriminate between DalvikBridgeFunc and
     * DalvikNativeFunc; the former takes an argument superset (i.e. two
     * extra args) which will be ignored.  If necessary we can use
     * insns==NULL to detect JNI bridge vs. internal native.
     */
    DalvikBridgeFunc nativeFunc;

#ifdef WITH_PROFILER
    bool inProfile;
#endif
#ifdef WITH_DEBUGGER
    short debugBreakpointCount;
#endif

    bool fastJni;

    /*
     * JNI: true if this method has no reference arguments. This lets the JNI
     * bridge avoid scanning the shorty for direct pointers that need to be
     * converted to local references.
     *
     * TODO: replace this with a list of indexes of the reference arguments.
     */
    bool noRef;

} Method;

6.调用以及结果

实现 MainActivity 下的 fix 进行调用。

    public void fix(View view) {
        Log.i("tuch", " 路径  :  " + Environment.getExternalStorageDirectory());
        new DexManager(this).loadFile(new File(Environment.getExternalStorageDirectory(),"out.dex"));
    }

在 AndroidManifest.xml 添加文件读写权限。

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

这边没有实现服务器部分,直接把 dex 文件 push 到对应的位置。

结果:
这里写图片描述

直接点击计算的时候,程序崩溃,报除 0 异常。当先点击修复,在进行计算的时候,可以看见结果显示为 10,说明已经按我们修复后的代码进行 10 / 1 计算。

四、davlik 修复源码分析

1.思路

这边先贴一下 AndFix 0.5.0 中对 davlik 修复的核心代码。

dalvik_method_replace.cpp

static void* dvm_dlsym(void *hand, const char *name) {
    void* ret = dlsym(hand, name);
    char msg[1024] = { 0 };
    snprintf(msg, sizeof(msg) - 1, "0x%x", ret);
    LOGD("%s = %s\n", name, msg);
    return ret;
}

extern jboolean __attribute__ ((visibility ("hidden"))) dalvik_setup(
        JNIEnv* env, int apilevel) {
    void* dvm_hand = dlopen("libdvm.so", RTLD_NOW);
    if (dvm_hand) {
        dvmDecodeIndirectRef_fnPtr = dvm_dlsym(dvm_hand,
                apilevel > 10 ?
                        "_Z20dvmDecodeIndirectRefP6ThreadP8_jobject" :
                        "dvmDecodeIndirectRef");
        if (!dvmDecodeIndirectRef_fnPtr) {
            return JNI_FALSE;
        }
        dvmThreadSelf_fnPtr = dvm_dlsym(dvm_hand,
                apilevel > 10 ? "_Z13dvmThreadSelfv" : "dvmThreadSelf");
        if (!dvmThreadSelf_fnPtr) {
            return JNI_FALSE;
        }
        jclass clazz = env->FindClass("java/lang/reflect/Method");
        jClassMethod = env->GetMethodID(clazz, "getDeclaringClass",
                        "()Ljava/lang/Class;");

        return JNI_TRUE;
    } else {
        return JNI_FALSE;
    }
}

extern void __attribute__ ((visibility ("hidden"))) dalvik_replaceMethod(
        JNIEnv* env, jobject src, jobject dest) {
    jobject clazz = env->CallObjectMethod(dest, jClassMethod);
    ClassObject* clz = (ClassObject*) dvmDecodeIndirectRef_fnPtr(
            dvmThreadSelf_fnPtr(), clazz);
    clz->status = CLASS_INITIALIZED;

    Method* meth = (Method*) env->FromReflectedMethod(src);
    Method* target = (Method*) env->FromReflectedMethod(dest);
    LOGD("dalvikMethod: %s", meth->name);

//  meth->clazz = target->clazz;
    meth->accessFlags |= ACC_PUBLIC;
    meth->methodIndex = target->methodIndex;
    meth->jniArgInfo = target->jniArgInfo;
    meth->registersSize = target->registersSize;
    meth->outsSize = target->outsSize;
    meth->insSize = target->insSize;

    meth->prototype = target->prototype;
    meth->insns = target->insns;
    meth->nativeFunc = target->nativeFunc;
}

extern void dalvik_setFieldFlag(JNIEnv* env, jobject field) {
    Field* dalvikField = (Field*) env->FromReflectedField(field);
    dalvikField->accessFlags = dalvikField->accessFlags & (~ACC_PRIVATE)
            | ACC_PUBLIC;
    LOGD("dalvik_setFieldFlag: %d ", dalvikField->accessFlags);
}

上面手写的代码与 AndFix 主要思路都是去到要修复方法的底层指针,然后把这个指针指向修复后方法的具体实现。但是 AndFix 在修改方法的具体实现之前,先进行获取修复后的方法所在类,并把他的 status 置为CLASS_INITIALIZED。而手写实现的没有。

对于设置 CLASS_INITIALIZED 这个操作,个人邮件了作者,作者原话是: loadclass并没初始化啊,设置类的状态目的是为了跳过verify。我在代码中没有进行设置也是可以,再次邮件作者,没有回复,具体原因不太确定,目前个人还是感觉这一段是不需要的。

2.原理

主要原理都是在安卓App热补丁动态修复技术介绍 ,这边进行一个简单的概述。(下面原理、图片部分来源于此)

虚拟机在启动的时候,会有许多启动参数,其中一项就是 verify 选项,当 verify 选项被打开的时候,会对所加载的类进行一个验证的过程。

  1. 验证 clazz->directMethods 方法,directMethods 包含了以下方法:
    1. static方法
    2. private方法
    3. 构造函数
  2. clazz->virtualMethods
    1. 虚函数=override方法?

简单的说,如果一个在上面的方法中引用到的其他类,都与该类在同一个 dex 包下,那个这个类就会被 CLASS_ISPREVERIFIED 这个标记。

这里写图片描述

3.造成问题

当 apk 采用分包技术或者其他原因导致所在类在不同的 dex 文件下的时候。假如我们的 apk 下的 classes.dex 中有两个类 A 和 B,A 引用了 B,且 A 没有引用其他 dex 包下方法,则根据上方的原理, A 会被打上 CLASS_ISPREVERIFIED 这个标记。这时候如果说我们进行 B.class 类的修复,对 B.class 进行整个替换。

这里写图片描述

由于 A.class 被打上了 CLASS_ISPREVERIFIED 标记,只能引用 A 所在的 dex (即classes.dex)下的类,而 B.class 在 out.dex 中,故程序会报错。

替换 B.class:
这里写图片描述

所以,为了解决这个问题,需要避免 A.class 打上标志位 CLASS_ISPREVERIFIED,常用的一种方式就是在程序第一次打包的时候,建一个空的类 AntilazyLoad,这个类被打包成单独的 hack.dex。其他所有类都引用这个 AntilazyLoad 类,这样所有的类都不会被打上 CLASS_ISPREVERIFIED。
这里写图片描述

另外一种情况就是,我们要修复的类是 A.class,这样替换 A.class 后,为了能让 A.class 正常的调到 B.class,我们需要避免 out.dex 下的 A.class 打上 CLASS_ISPREVERIFIED 标记,所以手动把他的标志位修改为 CLASS_INITIALIZED,跳过 verify。
这里写图片描述

4.对于 AndFix

但是对于 AndFix 来说,只是通过替换了方法在虚拟机中的具体实现内容,而不是替换整个 B.class。假设 A.class 调用的是 B.class 下的 test()这个方法,test()就是我们要修复的方法。

这里写图片描述

我们只是替换了 B.class 下的 test()方法的具体实现,并没有修改类的引用。对于 A.class 来说,所引用的 B.class 还是 classes.dex 包下的 B.class,所以不会有 CLASS_ISPREVERIFIED 问题,不需要进行修改。

这里写图片描述

注:这个原理只是个人目前看法,未验证。作者说需要加上类状态,但是个人试验的时候没有加上类状态的设置时可以进行修复的,我无法解释为什么可以。
如果有能解释这个的大佬,请给我留言评论,非常感谢。

五、art 修复

理解了 davlik 虚拟机的修复, art 虚拟机的修复其实也是一样的,只是在 art 虚拟机中,不太版本的虚拟机的 Class、ArtMethod 的结构体有所不通,所以需要对对应的虚拟机采用不要的头文件,替换当前版本对应要替换的东西,具体可以参考 AndFix 的源码进行参看。

六、附

代码链接

猜你喜欢

转载自blog.csdn.net/qq_18983205/article/details/80042172