[热修复]动手实现sopfix热修复,整个替换ArtMethod

今天我们在最新的android版本(12,13)上实操Sophix的核心原理: 基于ArtMethod的整体替换方案. 首先回顾学习阿里的sopfix原理介绍
从文中可知, 最关键的2个技术点:

  • 找到要热修复的java方法对应的ArtMethod
  • 对ArtMethod进行替换
    是不是很简单, 看起来是这样. 但是目前由于android的art虚拟机安全性的加强, 并没有这么容易,

原文中有代码示例:

art::mirror::ArtMethod* smeth =
            (art::mirror::ArtMethod*) env->FromReflectedMethod(src);
art::mirror::ArtMethod* dmeth =
            (art::mirror::ArtMethod*) env->FromReflectedMethod(dest);

然而很遗憾, 当你拿来用时, 并不生效, 倒不是说运行报错, 而是返回的值, 明显不是内存地址. 在我的小米11手机上分别返回57, 59. 这明显不是内存地址.
看起来像是index序号之类的值. 回过头来看一眼我们这次要热修的方法定义:

public class SomeClass {
    public final static int f1() {
        return 100;
    }
    public final static int f2() {
        return 200;
    }
}

我们在SomeClass类中只定义了两个方法, 一个要被替换的方法f1(), 另一个则是要用来替换f1()的热修下发新方法f2() 这里为了演示方便, 写在一起了,现实中肯定要在补丁patch dex中动态下发f2(), 而这不是热修的关键技术, 先忽略.
回到上面的地址不对的问题, 怎么解决呢?
参考了epic的实现, 发现目前得到的返回值果然是序号.
/art/runtime/jni/jni_id_manager.cc

image.png

看起来仍然是在jni中正常逻辑处理不了的, 那就直接拿来用吧. 函数在libart.so中, 函数签名:_ZN3art3jni12JniIdManager14DecodeMethodIdEP10_jmethodID
如何使用, 请参考前面的文章[APM学习]如何在android N之后dlopen使用系统私有库
定义个本地函数指针JniIdManager_DecodeMethodId_指向libart.so中的_ZN3art3jni12JniIdManager14DecodeMethodIdEP10_jmethodID

JniIdManager_DecodeMethodId_ 
      = reinterpret_cast<void *(*)(void *, jlong)>
      (mydlsym(handle, "_ZN3art3jni12JniIdManager14DecodeMethodIdEP10_jmethodID"));

然后就是解析java方法的ArtMethod地址具体步骤:

// 通过反射先得到methodId, 就是上面说的57
size_t src_art_method = reinterpret_cast<size_t> (env->GetStaticMethodID(someClass, "f1",
                                                                             "()I"));
//然后转换为地址
src_art_method = reinterpret_cast<jlong>(JniIdManager_DecodeMethodId_(
                ArtHelper::getJniIdManager(), src_art_method));

同样的逻辑, 得到f1(),f2()的ArtMethod地址, 分别存放在smeth, dmeth指针中, 最后进行整体赋值替换.

    void *smeth =(void *) src_art_method;
    void *dmeth =(void *) dest_art_method;

    // art_method_length 就是ArtMehod的结构体占用的内存空间, 
    //也就是紧挨着的两个java方法的对应的地址的差
    size_t art_method_length = dest_art_method - src_art_method;
     // 整体替换
    memcpy(smeth, dmeth, art_method_length);

UI 代码

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());
        replace = new NativeArtMethodReplace();
        // 模拟显示一个错误结果
        updateTextView();

        binding.doReplace.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 模拟热修
                replace.hotfix();
                // 热修完成后,再次刷新页面显示
                updateTextView();
            }
        });
    }

    // 更新显示页面显示
    private void updateTextView() {
        TextView tv = binding.sampleText;
        tv.setText("100+100=" + SomeClass.f1());
    }

效果

image.png

点了"do Hotfix"模拟热修, 界面变成:
image.png

代码已经上传到github: ArtMethodReplaceDemo

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容