广告位联系
返回顶部
分享到

Android AndFix热修复原理介绍

Android 来源:互联网 作者:佚名 发布时间:2022-08-28 20:57:46 人浏览
摘要

前言 当我们写了一个方法,那么这个方法是如何被执行的呢? 1 2 3 4 5 public int add(){ int a = 10; int b = 20; return a + b; } 其实方法的本质就是arm指令,在Android当中,dalvik或者art虚拟机的执行

前言

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

1

2

3

4

5

public int add(){

    int a = 10;

    int b = 20;

    return a + b;

}

其实方法的本质就是arm指令,在Android当中,dalvik或者art虚拟机的执行引擎会执行arm指令 

 add方法是java代码,java代码编译成class文件,还需要一步转换为dex文件,才能被Android虚拟机执行,dex文件包含了app的所有代码,因此方法也是存在dex文件中,那么通过dx命令,可以查看方法被编译成的字节码指令

1 arm指令集

1

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指令集,ART执行某个方法的时候,执行的就是这个指令集,当apk安装的时候,dex文件会被dex2oat工具翻译成本地机器码(arm指令集)保存在oat文件中,当apk运行的时候oat会被加载到内存中,存在虚拟机的方法区中 

 执行的时候,会构建一个栈帧压入虚拟机栈中,然后每一个方法在ART中都对应一个ArtMethod(这个后边会说),ArtMethod中的invoke函数会找到当前方法对应的本地机器码执行,执行完成之后,栈帧出栈

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

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获取,这种跟arm指令有啥区别呢?其实都是字节码,但是javap获取的字节码是JVM执行的字节码,Android虚拟机是Dalvik或者Art虚拟机,执行的是arm指令集

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

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: iadd

         9: ireturn

      LineNumberTable:

        line 4: 0

        line 5: 3

        line 6: 6

这两者有什么区别呢?我们看同是执行 10 + 20 ,JVM是先创建一个10变量,然后再创建20 ,最后将两个相加然后返回;但是从ART机器指令中可以看到是直接计算好了,然后创建v0 = 30,直接返回,所以:Android编译器在编译的过程中会做优化,提高执行的效率(这个可以自己去试一下,javac并没有做优化处理)

当一个class类加载进来之后,class类中有方法、成员变量等,这些类的信息加载的时候是放在方法区,当Java层调用某个方法时,ART虚拟机找到该方法对应的本地机器码指令,在虚拟机栈中,该方法栈帧入栈,CPU去读取每行指令,程序计数器+1,等到方法执行完毕,栈帧出栈。

2 AndFix热修复原理

之前我们介绍过阿里的AndFix或者Sophix是通过hook native层替换已经加载的类的方法,接下来我们着重看一下,AndFix热修复是怎么实现的

1

2

3

4

5

6

7

8

9

10

11

12

13

14

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"

1

2

3

4

5

6

7

8

9

10

11

public class Method {

 

    public int add(){

        int a = 10;

        int b = 20;

        return a + b;

    }

}

//调用

Method method = new Method();

method.add();

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

当执行这个方法时,ART执行引擎从方法区中找到方法的本地机器指令,通过CPU执行得到结果,如果add方法中抛出异常导致app崩溃,那么如何修复?

2.1 ArtMethod

既然要做到方法替换,首先必须要了解方法在虚拟机中的形态;其实前面有提到,方法在虚拟机中对应的结构体就是ArtMethod,每个方法在ART中对应一个ArtMethod。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

# 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_,这个指针指向的就是在方法区中该方法本地机器码的内存地址,也就是说,如果想要实现热修复,那么就将entry_point_from_quick_compiled_code_指向正确的方法机器码指令地址即可。

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

  • declaring_class_:用来标记当前方法属于哪个类
  • access_flags_:当前方法的访问修饰符
  • hotness_count_:记录当前方法被调用的次数,如果超过某个限制,那么该方法就被标记为是热方法,这个与ART的编译模式相关

对于hotness_count_,这里需要说一下ART的编译模式,Dalvik的就先不介绍了

2.2 ART编译模式

在Android 5.0之后,Android编译器由ART代替了Dalvik,采用了全新的编译模式AOT,代替JIT;

什么是AOT?就是全量编译,在APK安装的时候,会将所有的dex文件编译成本地机器码,然后在执行方法时会直接拿到相应的机器码执行,速度非常快,但是这也带来一些问题:

(1)安装时间长

因为在安装的过程中做全量的编译,耗时非常严重;早先的Android手机我们在安装的时候,进度条一直在转但就是装不上,这种是非常差的用户体验

(2)存储空间

因为全量编译的时候,dex被编译成机器码之后,保存在.oat文件中,10M的dex翻译成的机器码内存激增4-5倍,大量的文件保存在手机中会占据内存空间

所以在Android N之后,采用了混合编译模式,AOP + 解释 + JIT

全新的混编模式不再在APK安装的时候进行全量编译,而是会解释字节码,因此安装的速度很快;此外新增了一个JIT编译器,会在App运行的时候分析代码,把结果保存在Profile中,并且在空闲时间分析并编译这些代码;

接着上面的hotness_count_,其实用来记录这个方法被调用的次数,当超过某个阈值之后,这个方法会被标记为热代码,这些热方法在设备空闲的时候做编译,并保存在名为app_image的base.art文件中,这个art文件会在类加载之前加载到内存中,意味着当调用这个方法的时候,不再需要编译为机器码,而是直接执行拿到结果。

2.3 AndFix框架实现

首先创建一个C++的模块,然后C++版本可选择个人熟悉的,我对C++ 11的一些特性比较熟悉 

 其实AndFix实现的关键,就是找到ArtMethod,在JNI层是能够实现的,通过JNIEnv的FromReflectedMethod函数

1

2

3

4

public class AndFixManager {

    //native热修复方法

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

}

1

2

3

4

5

6

7

8

9

10

11

12

13

//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,只不过再底层的我们看不到,那现在就能看到了!

1

2

3

4

5

6

7

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如下;

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

#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

1

2

3

4

5

6

7

8

9

10

11

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 方法替换

1

2

3

4

5

6

public class FixDemo {

 

    public void run(){

        throw new IllegalArgumentException();

    }

}

1

2

3

4

5

public class FixDemo {

    public void run(){

        Log.e("TAG","已经被修复了");

    }

}

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

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方法

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

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方法

1

2

3

4

5

6

binding.sampleText.setOnClickListener {

    AndFixManager.bugFix()

    val fixDemo = FixDemo()

    fixDemo.run()

}

打印出的结果:E/TAG: 已经被修复了

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

2.4 AndFix动态化配置

在上面简单的demo中,我们是知道那个类的哪个方法发生异常,在代码中写死的,但真正的线上环境中,其实是不知道哪个类会报错,一般我们都会使用bugly,像crash跟anr都能够实时监控到 

在这里插入图片描述

 当app某个方法抛异常之后,通过bugly上报到后台,比如com.take.andfix.FixDemo这个类中的run方法抛出了异常,那么我们需要针对这个类的方法做修复,如果做到动态化,需要使用注解修饰这个修复类

1

2

3

4

5

6

7

8

9

/**

 * 修复类需要使用这个注解

 */

@Target(ElementType.METHOD)

@Retention(RetentionPolicy.RUNTIME)

public @interface andfix {

    String clazz();

    String method();

}

1

2

3

4

5

6

public class FixDemo {

    @andfix(clazz = "com.tal.andfix.FixDemo",method = "run")

    public void run(){

        Log.e("TAG","已经被修复了");

    }

}

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

2.4.1 dex打包

在打包dex的时候,需要把整个包名路径下的class文件一起打包,通过命令行完成dex打包

1

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就可以将这个类通过类加载器加载。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

/**

* 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

2

3

4

//方式1

Class.forName("xxxxxxxxxx")

//方式2

odex.loadClass(clazzName, context.getClassLoader())

2.4.3 动态替换方法

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

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补丁包来修复

1

2

3

4

5

6

7

8

9

10

binding.sampleText.setOnClickListener {

 

//            AndFixManager.bugFix()

    AndFixManager.loadFixDex(

        this,

        File(System.getenv("EXTERNAL_STORAGE"), "fix.dex")

    )

    val fixDemo = FixDemo()

    fixDemo.run()

}

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

1

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

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

1

android:requestLegacyExternalStorage="true"

通过这种hook native底层的方式,最大的优势在于能够真正实现热修复,不需要重新启动app就能够修复,但是存在的弊端也是比较明显的,就是兼容性问题,每个Android的版本,native层都会有变化,比如art_method.h,其实每个版本都是不一样的,我这次使用的就是Android 10中的art_method头文件,有兴趣的可以看看之前Android版本的头文件,其实还是有差别的,所以在做兼容性问题的时候,需要根据版本来适配不同的头文件


版权声明 : 本文内容来源于互联网或用户自行发布贡献,该文观点仅代表原作者本人。本站仅提供信息存储空间服务和不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权, 违法违规的内容, 请发送邮件至2530232025#qq.cn(#换@)举报,一经查实,本站将立刻删除。
原文链接 : https://juejin.cn/post/7100079518756552734
相关文章
  • Kotlin的Collection与Sequence操作异同点介绍

    Kotlin的Collection与Sequence操作异同点介绍
    在Android开发中,集合是我们必备的容器,Kotlin的标准库中提供了很多处理集合的方法,而且还提供了两种基于容器的工作方式:Collection 和
  • 实现一个Kotlin函数类型方法

    实现一个Kotlin函数类型方法
    接口与函数类型 业务开发中,经常会有实现一个函数式接口(即接口只有一个方法需要实现)的场景,大家应该都会不假思索的写出如下代
  • Android10 App启动Activity源码分析
    ActivityThread的main方法 让我们把目光聚焦到ActivityThread的main方法上。 ActivityThread的源码路径为/frameworks/base/core/java/android/app/ActivityThread。 1 2
  • Android10客户端事务管理ClientLifecycleManager源码解析

    Android10客户端事务管理ClientLifecycleManager源码解析
    在Android 10 App启动分析之Activity启动篇(二)一文中,简单地介绍了Activity的生命周期管理器是如何调度Activity进入onCreate生命周期的流程。这
  • Kotlin对象的懒加载方式by lazy与lateinit异同介绍

    Kotlin对象的懒加载方式by lazy与lateinit异同介绍
    属性或对象的延时加载是我们相当常用的,一般我们都是使用 lateinit 和 by lazy 来实现。 他们两者都是延时初始化,那么在使用时那么他们两
  • Android类加载流程分析

    Android类加载流程分析
    本文分析的代码基于Android8.1.0源码。 流程分析 从loadClass开始,我们来看下Android中类加载的流程 /libcore/ojluni/src/main/java/java/lang/ClassLoader.ja
  • Android实现读写USB串口数据的代码

    Android实现读写USB串口数据的代码
    最近在研究USB方面的内容;先后做了关于Android读写HID、串口设备的DEMO。本文比较简单,主要介绍的是Android实现读取串口数据的功能 废话不
  • Epoxy - 在RecyclerView中构建复杂界面
    Diffing 对于复杂数据结构支持的多个视图类型展示在屏幕上, Epoxy此时是尤其有用的. 在这些场景中, 数据可能会被网络请求, 异步 Observable, 用
  • Android性能优化的详细介绍

    Android性能优化的详细介绍
    性能优化是一个app很重要的一部分,一个性能优良的app从被下载到启动到使用都能给用户到来很好的体验。自然我们做性能优化也是从被下
  • Android进阶宝典-插件化2(Hook启动插件中四大组件

    Android进阶宝典-插件化2(Hook启动插件中四大组件
    在上一节,我们主要介绍了如果通过反射来加载插件中的类,调用类中的方法;既然插件是一个apk,其实最重要的是启动插件中的Activity、
  • 本站所有内容来源于互联网或用户自行发布,本站仅提供信息存储空间服务,不拥有版权,不承担法律责任。如有侵犯您的权益,请您联系站长处理!
  • Copyright © 2017-2022 F11.CN All Rights Reserved. F11站长开发者网 版权所有 | 苏ICP备2022031554号-1 | 51LA统计