阿里AndFix与sophix热修复原理解析

一、首先需要了解java的内存管理机制,大致分为3类

  • 方法区
  • 堆区
  • 栈区

三种内存区域定义如下:

方法区:当JVM使用类加载器定位class文件,并将其输入到内存中时,会提取class文件的类型信息,并将这些信息存储到方法区中。同时放入方法区中的还有该类型中的类静态变量。

堆内存:java程序在运行时创建的所有类型对象和数组都存储在堆中。JVM会根据new指令在堆中开辟一个确定类型的对象内存空间。但是堆中开辟对象的控件并没有任何人工指令可以回收,而是通过JVM的垃圾回收期负责回收。

Java栈:每启动一个线程(main),JVM都会为他分配一个java栈,用于存放方法中的局部变量,操作数以及异常数据等,当线程调用某个方法时,JVM会根据方法区中该方法的字节码组建一个栈帧,并将该栈帧压入java栈中,方法执行完毕时,JVM会弹出该栈帧并释放掉。所以,我们app应用被压入的第一个栈帧就是ActiviytThread类的main()方法。当main被释放掉时,我们的app就退出了。

本地方法栈:native堆

二、内存运行机制

类在没加载之前会在方法区声明一个int类型符号变量,它指向即将要加载内存的class类信息。当前类的字节码文件中的类信息,包括方法,成员变量。虚拟机加载的时候不会原封不动的把字节码信息加载到内存,而是会做一些调整,其中一点就会创建一个方法表(方法表实际上在native层是ArtMethod数组,方法表中的每一个方法都是ArtMethod结构体),这个方法表包含了这个类的所有方法(字节码转内存过程)。

类是从本地apk文件加载的,当我们安装一个apk时,会拷贝一份apk文件到系统data/app/xxxde1目录下,启动app应用就会通过dexfile读取第一个文件ActivityThread字节码文件,找到可执行的方法main,然后从类的方法表中将main方法压栈,形成一个栈帧。在ActivityThread类中会声明一个app加载的第二个类是application类。执行流程一样,会将onCreate方法压入栈,形成栈帧。所以方法的执行时通过压栈的方式执行的,方法的执行也是需要内存的(方法的执行最终是转为ArtMethod结构体去执行的)。伪代码如下:

class ActivityThread{
 Application application;
    public static void main(string[] args){
        application = new Application();
        application.onCreate(...);
    }
    


}

在ActivityThread类中声明了Application 类,此时类是没有被加载到内存的,只有在主动引用的时候才会被加载到内存(new操作符、反射class.forName、jni.findClass等)。执行到这里只会在方法区创建一个int类型符号变量。当执行到new的时候,会从我们的apk文件中找到字节码文件,在方法区为它开辟一个存储空间(创建方法表),并且当前的符号变量指向它。此时,class类还没有加载完成,同时在堆区开辟另外一块内存,存放对象的变量信息,这个对象会指向符号变量(实际是通过klass指向它,klass是类的载体,是唯一的)。这就是通过对象能够找到类的原因,伪代码如下:

Student stu = new Student();
Class clazz =stu.getClass();//通过对象找到类

对象内存分配完毕,此时执行到对象的onCreate方法,然后对象会送一个事件通过符号变量找到方法表中的该方法,并将它压栈,形成一个onCreate栈帧。

三、热修复分类:两大类

  • native层:andfix、sophix
  • java层:tinker、robsut

热升级:增量升级

两者异同点:都需要旧的apk与新的apk进行比对,生成差分包。实现的方式是一样的。不同点,目的性不一样

热修复是针对bug,热升级是根据版本升级。

四、AndFix

andFix是通过替换方法表中的方法实现热修复的,是native层的实现

五、手写AndFix修复

1、模拟一个bug类,其中有一个方法抛异常,代码如下:

package com.xinyartech.andfix.bug;
/**
 * <pre>
 *     desc    : bug类
 * </pre>
 */
public class AndFixTool {
    public int testMethod(){
        throw new RuntimeException("报异常了,需要修复");
    }
}

2、定义注解类,标明需要修复的类及方法,代码如下:

package com.xinyartech.andfix;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * <pre>
 *     desc    : 注解类 获取需要修复的类及方法
 * </pre>
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FixAnnotation {
    String clazz();//修复哪一个类
    String method();//替换哪一个方法
}

3、模拟一个修复类,修复该方法,在修复方法上加入修复类的全路径,及方法名,代码如下:

package com.xinyartech.andfix.fix;

import android.util.Log;

import com.xinyartech.andfix.FixAnnotation;

/**
 * <pre>
 *     desc    : 修复类
 * </pre>
 */
public class AndFixTool {
    @FixAnnotation(clazz = "com.xinyartech.andfix.bug.AndFixTool",method = "testMethod")
    public int testMethod(){
        Log.e("AndFixTool","已经修复了");
        return 10;
    }
}

4、借助android studio编译,build一下项目,生成字节码文件。将编译的字节码文件夹拷贝到桌面,备后续使用,注意:红色的是包含包名的路径

5、删除和修复包无用的字节码文件,如下:

6、使用dx.bat命令将当前修复包打成dex文件,命令如下,需要在环境变量中配置dx命令

7、执行dx生成一个dex文件

这个fix.dex文件就是我们的修复文件,一般需要我们将这个文件放到服务器,供用户下载,目前我们手动把修复包放到手机sd卡

8、在Activity中模拟修复方法,fixMethod(),代码如下:

public void fixMethod(View view) {
        File file = new File(Environment.getExternalStorageDirectory(), "fix.dex");
        DexManager dexManager = new DexManager(this);
        dexManager.load(file);
}

DexManager代码如下:

public class DexManager {
    private Context context;

    public DexManager(Context context) {
        this.context = context;
    }
    public void load(File file) {
        try {
            //通过dexFile下载dex文件,这个是fix.dex
            DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),
                    new File(context.getCacheDir(), "opt").getAbsolutePath(), Context.MODE_PRIVATE);
            //获取当前的dex里面的class 类名集合
            Enumeration<String> entry=dexFile.entries();
            while (entry.hasMoreElements()) {
                String clazzName= entry.nextElement();
                Class realClazz= dexFile.loadClass(clazzName, context.getClassLoader());
                if (realClazz != null) {
                    fixClazz(realClazz);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    private void fixClazz(Class realClazz) {
        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 {
                //获取有bug的class
                Class wrongClazz=  Class.forName(clazzName);
                //获取有bug的方法
                Method wrongMethod = wrongClazz.getDeclaredMethod(methodName, rightMethod.getParameterTypes());
                //native替换方法
                replace(wrongMethod, rightMethod);

            } catch (Exception e) {
                e.printStackTrace();
            }
        }



    }
    //native层方法
    public  native static void replace(Method wrongMethod, Method rightMethod) ;
}

9、native层实现

(1)需要拷贝系统的和ArtMethod相关的两个文件,目录如下:E:\BaiduNetdiskDownload\Android系统源码\android-5.0.0_r7\android-5.0.0_r7\art\runtime\mirror。。文件名为object.h和art_method.h拷贝到cpp目录下。然后在natice-lib.cpp中添加如下代码:

#include <jni.h>
#include <string>
#include "art_method.h"//引入头文件
extern "C" JNIEXPORT jstring JNICALL
JNIEXPORT void JNICALL
Java_com_xinyartech_andfix_DexManager_replace(JNIEnv *env, jclass type, jobject wrongMethod,
                                              jobject rightMethod) {
//ArtMethod  Android 系统源码中
    art::mirror::ArtMethod *wrong=  (art::mirror::ArtMethod *)env->FromReflectedMethod(wrongMethod);
    art::mirror::ArtMethod *right=  (art::mirror::ArtMethod *)env->FromReflectedMethod(rightMethod);
//    method   --->class  --->ClassLoader
    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->method_index_ = right->method_index_;
    wrong->dex_method_index_ = right->dex_method_index_;



}

注意:这里引入的是5.0系统的.h文件,所以只适用5.0系统的热修复,这也是andfix严重缺陷。如果需要其他版本的也能够兼容,就需要对不同的系统文件进行引入

原因是这样的,每一个系统版本谷歌工程师都会对ArtMethod结构体动刀,导致有些参数的长度有变化,比如在5.0系统是32位的,在6.0可能就会被改成16位,这样改会导致我们的结构体结构会发生变化,发生溢出。但我们实际操控结构体的时候,就会发现有些变量找不到的情况。其实阿里开源的AndFix针对每个系统版本都做了兼容,不过目前只到7.0系统,后面的版本没有继续更新维护了。截图如下:

要解决所有版本的兼容问题,那么阿里推出了sophix热修复,它是在andfix的基础上解决了上面的问题,但是它是收费的,5000台手机免费。其实解决思路应该就是artmethod结构体的大小不能有变化。但是,目前不得而知。

猜你喜欢

转载自blog.csdn.net/qinbin2015/article/details/90442354