今天我们在最新的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