今天我們在最新的android版本(12,13)上實(shí)操Sophix的核心原理: 基于ArtMethod的整體替換方案. 首先回顧學(xué)習(xí)阿里的sopfix原理介紹
從文中可知, 最關(guān)鍵的2個技術(shù)點(diǎn):
- 找到要熱修復(fù)的java方法對應(yīng)的ArtMethod
- 對ArtMethod進(jìn)行替換
是不是很簡單, 看起來是這樣. 但是目前由于android的art虛擬機(jī)安全性的加強(qiáng), 并沒有這么容易,
原文中有代碼示例:
art::mirror::ArtMethod* smeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(src);
art::mirror::ArtMethod* dmeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(dest);
然而很遺憾, 當(dāng)你拿來用時, 并不生效, 倒不是說運(yùn)行報錯, 而是返回的值, 明顯不是內(nèi)存地址. 在我的小米11手機(jī)上分別返回57, 59. 這明顯不是內(nèi)存地址.
看起來像是index序號之類的值. 回過頭來看一眼我們這次要熱修的方法定義:
public class SomeClass {
public final static int f1() {
return 100;
}
public final static int f2() {
return 200;
}
}
我們在SomeClass類中只定義了兩個方法, 一個要被替換的方法f1(), 另一個則是要用來替換f1()的熱修下發(fā)新方法f2() 這里為了演示方便, 寫在一起了,現(xiàn)實(shí)中肯定要在補(bǔ)丁patch dex中動態(tài)下發(fā)f2(), 而這不是熱修的關(guān)鍵技術(shù), 先忽略.
回到上面的地址不對的問題, 怎么解決呢?
參考了epic的實(shí)現(xiàn), 發(fā)現(xiàn)目前得到的返回值果然是序號.
/art/runtime/jni/jni_id_manager.cc

image.png
看起來仍然是在jni中正常邏輯處理不了的, 那就直接拿來用吧. 函數(shù)在libart.so中, 函數(shù)簽名:
_ZN3art3jni12JniIdManager14DecodeMethodIdEP10_jmethodID如何使用, 請參考前面的文章[APM學(xué)習(xí)]如何在android N之后dlopen使用系統(tǒng)私有庫
定義個本地函數(shù)指針
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"));
//然后轉(zhuǎn)換為地址
src_art_method = reinterpret_cast<jlong>(JniIdManager_DecodeMethodId_(
ArtHelper::getJniIdManager(), src_art_method));
同樣的邏輯, 得到f1(),f2()的ArtMethod地址, 分別存放在smeth, dmeth指針中, 最后進(jìn)行整體賦值替換.
void *smeth =(void *) src_art_method;
void *dmeth =(void *) dest_art_method;
// art_method_length 就是ArtMehod的結(jié)構(gòu)體占用的內(nèi)存空間,
//也就是緊挨著的兩個java方法的對應(yīng)的地址的差
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();
// 模擬顯示一個錯誤結(jié)果
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
點(diǎn)了"do Hotfix"模擬熱修, 界面變成:

image.png
代碼已經(jīng)上傳到github: ArtMethodReplaceDemo