IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Android进阶宝典 --- AndFix热修复原理解析 -> 正文阅读

[移动开发]Android进阶宝典 --- AndFix热修复原理解析

当我们写了一个方法,那么这个方法是如何被执行的呢?

public int add(){
    int a = 10;
    int b = 20;
    return a + b;
}

其实方法的本质就是arm指令,然后JVM的执行引擎会执行arm指令
在这里插入图片描述
add方法是java代码,java代码编译成class文件,还需要一步转换为dex文件,才能被Android虚拟机执行,arm指令是存在于dex文件中的,也就是说,我们可以从dex文件中取出arm指令,查看一个方式是如何被执行的

1 arm指令集

dx --dex --verbose --dump-to=dex_method.txt --dump-method=Method.add --verbose-dump Method.class 

Android中可以通过dx命令将class文件转换为dex文件,dx.bat位于Android SDK中的build-tools文件夹下,那么可以通过dx命令将class文件翻译成arm指令集
在这里插入图片描述
我看可以看一下,打印输出的arm指令集,Android虚拟机执行某个方法的时候,执行的就是这个指令集,这种指令集在加载的时候,存在JVM虚拟机的方法区中
在这里插入图片描述
执行的时候,JVM的执行引擎会将arm指令从方法区中拿出来,放到虚拟机栈中执行(栈帧的概念,每一个方法对应的dx指令集就是一个栈帧,每一次方法调用都有栈帧入栈和出栈)

关注点回到指令集上,在每一行指令前有一个数字,代表程序计数器记录的行号,精简之后的指令集(只保留每个行号的最后一个)

Method.add:()I:
regs: 0002; ins: 0001; outs: 0000
 
  0000: const/16 v0, #int 30 // #001e
  0002: return v0
  0003: code-address
  debug info
    line_start: 4
    parameters_size: 0000
    0000: prologue end
    0000: line 4
    0000: line 6
    end sequence
  source file: "Method.java"

另外还有一种方式获取字节码,是通过javap获取,这种跟dex指令有啥区别呢?其实都是字节码,但是javap获取的字节码是JVM执行的字节码,Android虚拟机是Dalvik或者Art虚拟机,执行的是dx指令集

public int add();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: bipush        10
         2: istore_1
         3: bipush        20
         5: istore_2
         6: iload_2
         7: iload_1
         8: idiv
         9: ireturn
      LineNumberTable:
        line 4: 0
        line 5: 3
        line 6: 6

这两者有什么区别呢?我们看同是执行 10 + 20 ,JVM是先创建一个10变量,然后再创建20 ,最后将两个相加然后返回;但是dx指令是直接计算好了,然后创建v0 = 30,直接返回,所以:Android编译器在编译的过程中会做优化,提高执行的效率

当一个class类加载进来之后,class类中有方法、成员变量等,这些类的信息加载的时候是放在方法区,当Java层调用某个方法时,执行引擎从方法区拿出dx指令集,作为栈帧拷贝到虚拟机栈(高速缓存区),CPU去读取每行指令,程序计数器+1,等到方法执行完毕,栈帧出栈。

2 AndFix热修复原理

之前我们介绍过阿里的AndFix或者Sophix是通过hook native层修改字节码指令完成,之前我们介绍的arm指令集,就是实现热修复的基础。

Method.add:()I:
regs: 0002; ins: 0001; outs: 0000
 
  0000: const/16 v0, #int 30 // #001e
  0002: return v0
  0003: code-address
  debug info
    line_start: 4
    parameters_size: 0000
    0000: prologue end
    0000: line 4
    0000: line 6
    end sequence
  source file: "Method.java"
public class Method {

    public int add(){
        int a = 10;
        int b = 20;
        return b / a;
    }
}
//调用
Method method = new Method();
method.add();

我们看下这个方法,通过Method对象去调用,method是在堆内存中,通过对象可以拿到类信息在方法区中。
在这里插入图片描述

当执行这个方法时,执行引擎将该方法arm指令取出,放在虚拟机栈中执行,得到结果,如果add方法中抛出异常导致app崩溃,那么如何修复?

2.1 异常arm指令

如果方法中抛出了异常,那么编译成的arm指令如下

Method.add:()I:
regs: 0002; ins: 0001; outs: 0000
 
  0000: invoke direct (v0),java.lang.IllegalArgumentException
  0002: throw v0
  source file: "Method.java"

当CPU执行到这个栈帧的时候,就会抛出异常;

所以AndFix热修复,就是将正确的arm指令替换调异常的arm指令,等到再次加载这个类执行方法时,执行引擎拿到的是正确的arm指令交由虚拟机栈。

2.2 ArtMethod

所以如果想要替换掉这个错误的方法,首先必须要了解方法在虚拟机中的形态;其实,方法在虚拟机中对应的结构体就是ArtMethod,每个方法在art中对应一个ArtMethod。

# Android 10.0/art/runtime/art_method.h
protected:
 
  GcRoot<mirror::Class> declaring_class_;
  
  std::atomic<std::uint32_t> access_flags_;

  uint32_t dex_code_item_offset_;

  uint32_t dex_method_index_;
  uint16_t method_index_;

  union {
    uint16_t hotness_count_;
    uint16_t imt_index_;
  };

  // Fake padding field gets inserted here.

  // Must be the last fields in the method.
  struct PtrSizedFields {
    // Depending on the method type, the data is
    //   - native method: pointer to the JNI function registered to this method
    //                    or a function to resolve the JNI function,
    //   - conflict method: ImtConflictTable,
    //   - abstract/interface method: the single-implementation if any,
    //   - proxy method: the original interface method or constructor,
    //   - other methods: the profiling data.
    void* data_;

    // Method dispatch from quick compiled code invokes this pointer which may cause bridging into
    // the interpreter.
    void* entry_point_from_quick_compiled_code_;
  } ptr_sized_fields_;

在ArtMethod中,有一个结构体PtrSizedFields,其中一个成员变量为entry_point_from_quick_compiled_code_,当前这个指针指向的就是arm指令的内存地址,也就是说,如果想要实现热修复,那么就将entry_point_from_quick_compiled_code_指向正确的方法arm指令地址即可。

除此之外,看下其他成员变量的含义:

declaring_class_:用来标记当前方法属于哪个类
access_flags_:当前方法的访问修饰符
hotness_count_:记录当前方法被调用的次数,如果超过某个限制,那么该方法就被标记为是热方法,热方法是被缓存到一块内存,下次执行到这个方法,不需要压栈,直接返回结果

2.3 AndFix框架实现

首先创建一个C++的模块,然后C++版本可选择个人熟悉的,我对C++ 11的一些特性比较熟悉
在这里插入图片描述
其实AndFix实现的关键,就是找到ArtMethod,在JNI层是能够实现的,通过JNIEnv的FromReflectedMethod函数

public class AndFixManager {
    //native热修复方法
    public static native void fix(Method wrong, Method right);
}
//fix对应的JNI接口
extern "C"
JNIEXPORT void JNICALL
Java_com_tal_andfix_AndFixManager_fix(
        JNIEnv *env,
        jclass clazz,
        jobject wrong,
        jobject right) {

    //获取ArtMethod
    env->FromReflectedMethod(wrong);

}

其实在Java层调用的时候,是需要反射获取某个方法,也就是说,在Java层反射拿到的方法其实就是ArtMethod,只不过再底层的我们看不到,那现在就能看到了!

try {
    Class<?> clazz = Class.forName("com.tal.demo02.FixDemo");
    Method run = clazz.getDeclaredMethod("run");
    AndFixManager.fix(run,run);
} catch (Exception e) {
    e.printStackTrace();
}

2.3.1 获取ArtMethod

之前我们看源码的时候,可以看到ArtMethod.h中存在很多系统的头文件,全部导入工程中不现实
在这里插入图片描述
因为我们需要的是ArtMethod的一个结构体的成员变量,所以我们只需要针对性地导入即可,art_method.h如下

#ifndef DEMO02_ART_METHOD_H
#define DEMO02_ART_METHOD_H

#endif //DEMO02_ART_METHOD_H

#include "stdint.h"

namespace art{

    namespace mirror{

        class ArtMethod final {

        public:

            uint32_t declaring_class_;

            std::atomic<std::uint32_t> access_flags_;

            uint32_t dex_code_item_offset_;

            uint32_t dex_method_index_;
            uint16_t method_index_;

            union {
                uint16_t hotness_count_;
                uint16_t imt_index_;
            };
            struct PtrSizedFields {
                void* data_;

                void* entry_point_from_quick_compiled_code_;
            } ptr_sized_fields_;

        };

    }
}

最终在Java层调用JNI方法,执行到JNI层,获取到ArtMethod

extern "C"
JNIEXPORT void JNICALL
Java_com_tal_andfix_AndFixManager_fix(
        JNIEnv *env,
        jclass clazz,
        jobject wrong,
        jobject right) {

    //获取ArtMethod
    ArtMethod *artMethod = reinterpret_cast<ArtMethod *>(env->FromReflectedMethod(wrong));
}

这里通过断点可以看到,ArtMethod已经拿到了,而且关键信息entry_point_from_quick_compiled_code_,也就是arm指令集的内存地址拿到了!
在这里插入图片描述

2.3.2 方法替换


public class FixDemo {

    public void run(){
        throw new IllegalArgumentException();
    }
}
public class FixDemo {
    public void run(){
        Log.e("TAG","已经被修复了");
    }
}

现在有一个场景就是,当执行FixDemo的run方法时抛出异常导致崩溃,这种场景下,使用热修复技术怎么修复呢,就是方法替换,arm指令集替换

public class AndFixManager {

    public static void bugFix(){
        try {
            Class clazz = Class.forName("com.take.andfix.FixDemo");
            Method wrong = clazz.getDeclaredMethod("run");
            //正确的方法
            Class clazz1 = Class.forName("com.take.andfix.fox.FixDemo");
            Method right = clazz1.getDeclaredMethod("run");
            AndFixManager.fix(wrong, right);
        }catch (Exception e){

        }
    }

    public static native void fix(Method wrong, Method right);
}

抛出异常的类是andfix包下的,当线上需要修复时,下发patch包,然后加载fox包下的方法,调用native fix方法

extern "C"
JNIEXPORT void JNICALL
Java_com_tal_andfix_AndFixManager_fix(JNIEnv *env, jclass clazz, jobject wrong, jobject right) {

    //获取ArtMethod
    ArtMethod *wrongMethod = reinterpret_cast<ArtMethod *>(env->FromReflectedMethod(wrong));
    ArtMethod *rightMethod = reinterpret_cast<ArtMethod *>(env->FromReflectedMethod(right));

    //方法替换
    wrongMethod->declaring_class_ = rightMethod->declaring_class_;
    wrongMethod->access_flags_ = rightMethod->access_flags_;
    wrongMethod->dex_code_item_offset_ = rightMethod->dex_code_item_offset_;
    wrongMethod->dex_method_index_ = rightMethod->dex_method_index_;
    wrongMethod->method_index_ = rightMethod->method_index_;
    wrongMethod->ptr_sized_fields_.data_ = rightMethod->ptr_sized_fields_.data_;
    wrongMethod->ptr_sized_fields_.entry_point_from_quick_compiled_code_ = rightMethod->ptr_sized_fields_.entry_point_from_quick_compiled_code_;
}

然后再次执行run方法

binding.sampleText.setOnClickListener {

    AndFixManager.bugFix()

    val fixDemo = FixDemo()
    fixDemo.run()
}
打印出的结果:E/TAG: 已经被修复了

其实现在阿里的AndFix和Sophix已经不维护了,但是这种热修复的思想我们是需要了解的,尤其是通过hook native底层修改arm指令集,能够帮助我们更好地了解JVM虚拟机和Android虚拟机。

2.4 AndFix动态化配置

在上面简单的demo中,我们是知道那个类的哪个方法发生异常,在代码中写死的,但真正的线上环境中,其实是不知道哪个类会报错,一般我们都会使用bugly,像crash跟anr都能够实时监控到
在这里插入图片描述
当app某个方法抛异常之后,通过bugly上报到后台,比如com.take.andfix.FixDemo这个类中的run方法抛出了异常,那么我们需要针对这个类的方法做修复,如果做到动态化,需要使用注解修饰这个修复类

/**
 * 修复类需要使用这个注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface andfix {
    String clazz();
    String method();
}
public class FixDemo {
    @andfix(clazz = "com.tal.andfix.FixDemo",method = "run")
    public void run(){
        Log.e("TAG","已经被修复了");
    }
}

这样在热修复时,能够知道这个修复类要修复线上环境中那个类的哪个方法

2.4.1 dex打包

在这里插入图片描述
在打包dex的时候,需要把整个包名路径下的class文件一起打包,通过命令行完成dex打包

dx --dex --output fix.dex /xxxx/Desktop/dx

在这里插入图片描述
将打包成功的dex修复包,放到sd卡中
在这里插入图片描述

2.4.2 dex文件加载

dex文件的加载,通过DexFile实现,如果不熟悉可以看下源码,art虚拟机会将dex转换为odex,因此加载dex文件的时候,需要传入一个odex文件的缓存路径。

将dex文件加载到内存之后,可以获取到dex文件中全部的类,通过DexFile.loadClass就可以将这个类通过类加载器加载。

/**
* dex文件加载,将dex文件加载到内存
* @param context
* @param dexFile
*/
private static void loadFixDex(Context context, File dexFile) {

   try {

       DexFile odex = DexFile.loadDex(
               dexFile.getAbsolutePath(),
               new File(context.getCacheDir(), "odex").getAbsolutePath(),
               Context.MODE_PRIVATE
       );
       Enumeration<String> entries = odex.entries();
       while (entries.hasMoreElements()){
           //全类名
           String clazzName = entries.nextElement();
           //加载类
           Class aClass = odex.loadClass(clazzName, context.getClassLoader());
           //处理类
           if(aClass != null){
               processClass(aClass);
           }
       }


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

这里会有一个问题,就是既然拿到了全类名,为什么不能通过方式1获取,而是需要通过方式2获取?原因就是,Class.forName是从当前apk中查找这个类,但是这个类是在dex文件中,是从服务端下发的,并没有放在apk中,因此通过Class.forName是找不到的,通过DexFile.loadClass才是真正加载类到了内存中

//方式1
Class.forName("xxxxxxxxxx")
//方式2
odex.loadClass(clazzName, context.getClassLoader())

2.4.3 动态替换方法

拿类之后,通过反射能够拿到修复类中的方法,当然不是每个方法都是需要被修复的,我们需要判断的是,上面是否有我们自定义的注解,如果有,那么就能够通过反射,拿到抛出异常的这个方法,因为注解上有我们传入的类名和方法名,最终调用JNI的接口实现动态替换方法

private static void processClass(Class aClass) {
    //获取方法上的注解
    Method[] methods = aClass.getMethods();
    for (Method method:methods){
        andfix annotation = method.getAnnotation(andfix.class);
        if(annotation != null){
            //如果存在这个注解,那么就执行方法替换
            String clazz = annotation.clazz();
            String method1 = annotation.method();
            //获取wrong方法
            try {
                Class<?> wrongMethodClass = Class.forName(clazz);
                //这里注意,修复类的方法,要和被修复的方法,参数一致!!!!!
                Method wrongMethod = wrongMethodClass.getDeclaredMethod(method1,method.getParameterTypes());
                //动态方法替换
                fix(wrongMethod,method);
            } catch (Exception e) {
                e.printStackTrace();
            }

        }
    }
}

2.4.4 文件访问问题

一切准备就绪之后,可以通过加载dex补丁包来修复

binding.sampleText.setOnClickListener {

//            AndFixManager.bugFix()
    AndFixManager.loadFixDex(
        this,
        File(System.getenv("EXTERNAL_STORAGE"), "fix.dex")
    )

    val fixDemo = FixDemo()
    fixDemo.run()
}

这里可能会碰到一些加载SD卡中文件报错的问题,比如:

No original dex files found for dex location /sdcard/fix.dex

这里需要添加文件的读写权限,才能够保证有效的热修复,除此之外,在Android 10以上的版本,需要在清单文件中添加android:requestLegacyExternalStorage属性

android:requestLegacyExternalStorage="true"
  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-05-24 18:20:14  更:2022-05-24 18:22:56 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/25 1:01:28-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码