JAndFix: 基于Java实现的Android实时热修复方案
簡述
- JAndFix是一種基于Java實(shí)現(xiàn)的Android實(shí)時(shí)熱修復(fù)方案,它并不需要重新啟動(dòng)就能生效。JAndFix是在AndFix的基礎(chǔ)上改進(jìn)實(shí)現(xiàn),AndFix主要是通過jni實(shí)現(xiàn)對method(ArtMethod)結(jié)構(gòu)題內(nèi)容的替換。JAndFix是通過Unsafe對象直接操作Java虛擬機(jī)內(nèi)存來實(shí)現(xiàn)替換。
原理
- 為何JAndfix能夠做到即時(shí)生效呢? 原因是這樣的,在app運(yùn)行到一半的時(shí)候,所有需要發(fā)生變更的Class已經(jīng)被加載過了,在Android上是無法對一個(gè)Class進(jìn)行卸載的。而騰訊系的方案,都是讓Classloader去加載新的類。如果不重啟,原來的類還在虛擬機(jī)中,就無法加載新類。因此,只有在下次重啟的時(shí)候,在還沒走到業(yè)務(wù)邏輯之前搶先加載補(bǔ)丁中的新類,這樣后續(xù)訪問這個(gè)類時(shí),就會(huì)Resolve為新的類。從而達(dá)到熱修復(fù)的目的。JAndfix采用的方法是,在已經(jīng)加載了的類中直接拿到Method(ArtMethod)在JVM的地址,通過Unsafe直接修改Method(ArtMethod)地址的內(nèi)容,是在原來類的基礎(chǔ)上進(jìn)行修改的。我們這就來看一下JAndfix的具體實(shí)現(xiàn)。
虛擬機(jī)調(diào)用方法的原理
為什么這樣替換完就可以實(shí)現(xiàn)熱修復(fù)呢?這需要從虛擬機(jī)調(diào)用方法的原理說起。在Android 6.0,art虛擬機(jī)中ArtMethod的結(jié)構(gòu)是這個(gè)樣子的:
@art/runtime/art_method.hclass ArtMethod FINAL {... ...protected:// Field order required by test "ValidateFieldOrderOfJavaCppUnionClasses".// The class we are a part of.GcRoot<mirror::Class> declaring_class_;// Short cuts to declaring_class_->dex_cache_ member for fast compiled code access.GcRoot<mirror::PointerArray> dex_cache_resolved_methods_;// Short cuts to declaring_class_->dex_cache_ member for fast compiled code access.GcRoot<mirror::ObjectArray<mirror::Class>> dex_cache_resolved_types_;// Access flags; low 16 bits are defined by spec.uint32_t access_flags_;/* Dex file fields. The defining dex file is available via declaring_class_->dex_cache_ */// Offset to the CodeItem.uint32_t dex_code_item_offset_;// Index into method_ids of the dex file associated with this method.uint32_t dex_method_index_;/* End of dex file fields. */// Entry within a dispatch table for this method. For static/direct methods the index is into// the declaringClass.directMethods, for virtual methods the vtable and for interface methods the// ifTable.uint32_t method_index_;// Fake padding field gets inserted here.// Must be the last fields in the method.// PACKED(4) is necessary for the correctness of// RoundUp(OFFSETOF_MEMBER(ArtMethod, ptr_sized_fields_), pointer_size).struct PACKED(4) PtrSizedFields {// Method dispatch from the interpreter invokes this pointer which may cause a bridge into// compiled code.void* entry_point_from_interpreter_;// Pointer to JNI function registered to this method, or a function to resolve the JNI function.void* entry_point_from_jni_;// 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_;... ... }這其中最重要的字段就是entry_point_from_interprete_和entry_point_from_quick_compiled_code_了,從名字可以看出來,他們就是方法的執(zhí)行入口。我們知道,Java代碼在Android中會(huì)被編譯為Dex Code。 art中可以采用解釋模式或者AOT機(jī)器碼模式執(zhí)行。
解釋模式,就是取出Dex Code,逐條解釋執(zhí)行就行了。如果方法的調(diào)用者是以解釋模式運(yùn)行的,在調(diào)用這個(gè)方法時(shí),就會(huì)取得這個(gè)方法的entry_point_from_interpreter_,然后跳轉(zhuǎn)過去執(zhí)行。
而如果是AOT的方式,就會(huì)先預(yù)編譯好Dex Code對應(yīng)的機(jī)器碼,然后運(yùn)行期直接執(zhí)行機(jī)器碼就行了,不需要一條條地解釋執(zhí)行Dex Code。如果方法的調(diào)用者是以AOT機(jī)器碼方式執(zhí)行的,在調(diào)用這個(gè)方法時(shí),就是跳轉(zhuǎn)到entry_point_from_quick_compiled_code_執(zhí)行。
AndFix的方法替換其本質(zhì)是ArtMethod指針?biāo)竷?nèi)容的替換。?
變成了這樣的整體替換?
由Unsafe來實(shí)現(xiàn)相當(dāng)于:
//src means source ArtMethod Address,dest mean destinction ArtMethod Addressprivate void replaceReal(long src, long dest) throws Exception {int methodSize = MethodSizeUtils.methodSize();int methodIndexOffset = MethodSizeUtils.methodIndexOffset();//methodIndex need not replace,becase the process of finding method in vtable need methodIndexint methodIndexOffsetIndex = methodIndexOffset / 4;//why 1? index 0 is declaring_class, declaring_class need not replace.for (int i = 1, size = methodSize / 4; i < size; i++) {if (i != methodIndexOffsetIndex) {int value = UnsafeProxy.getIntVolatile(dest + i * 4);UnsafeProxy.putIntVolatile(src + i * 4, value);}}}so easy,JAndFix就這樣完成了方法替換。值得一提的是,由于忽略了底層ArtMethod結(jié)構(gòu)的差異,對于所有的Android版本都不再需要區(qū)分,而統(tǒng)一以Unsafe實(shí)現(xiàn)即可,代碼量大大減少。即使以后的Android版本不斷修改ArtMethod的成員,只要保證ArtMethod數(shù)組仍是以線性結(jié)構(gòu)排列,就能直接適用于將來的Android 8.0、9.0等新版本,無需再針對新的系統(tǒng)版本進(jìn)行適配了。事實(shí)也證明確實(shí)如此,當(dāng)我們拿到Google剛發(fā)不久的Android O(8.0)開發(fā)者預(yù)覽版的系統(tǒng)時(shí)。
對比方案
| JAndFix | 阿里天貓 | JAVA | 支持 | 支持 | 不支持 | 4.0+ | ALL | 小 | 小 | 支持 | 好 |
| AndFix | 阿里支付寶 | C | 支持 | 支持 | 不支持 | 4.0+ | 極少部分不支持 | 小 | 小 | 支持 | 好 |
| Tinker | 騰訊 | JAVA | 不支持 | 支持 | 支持 | ALL | ALL | 小 | 小 | 不支持 | 好 |
| Robust | 美團(tuán) | JAVA | 支持 | 不支持 | 不支持 | ALL | ALL | 大 | 小 | 支持 | 差(需要反射調(diào)用,需要打包插件支持) |
| Dexposed | 個(gè)人 | C | 支持 | 支持 | 不支持 | 4.0+ | 部分不支持 | 小 | 小 | 支持 | 差(需要反射調(diào)用) |
如何使用
try {Method method1 = Test1.class.getDeclaredMethod("string");Method method2 = Test2.class.getDeclaredMethod("string");MethodReplaceProxy.instance().replace(method1, method2);} catch (Exception e) {e.printStackTrace();}Running DEMO
- 把整個(gè)項(xiàng)目放入你的IDE即可(Android Studio)
注意
Proguard
-keep class com.tmall.wireless.jandfix.MethodSizeCase { *;}解釋實(shí)現(xiàn)
- 我以Android Art 6.0的實(shí)現(xiàn)來解釋為什么這樣實(shí)現(xiàn)就可實(shí)現(xiàn)方法替換
1.declaring_class不能替換,為什么不能替換,是因?yàn)镴VM去調(diào)用方式時(shí)很多地方都要對declaring_class進(jìn)行檢查。替換declaring_class會(huì)導(dǎo)致未知的錯(cuò)誤。 2.methodIndex 不能替換,因?yàn)閜ublic proected等簡介尋址的訪問權(quán)限,本質(zhì)在尋找方法的時(shí)候會(huì)查找virtual_methods_,而virtual_methods_是個(gè)ArtMethod數(shù)組對象,需要通過methodIndex來查找,如果你的methodIndex不對會(huì)導(dǎo)致方法尋址出錯(cuò)。 3.為什么AbstractMethod類中對應(yīng)的artMethod屬性的值可以作為c層ArtMethod的地址直接使用?看源碼:
@@art/mirror/abstract_method.cc ArtMethod* AbstractMethod::GetArtMethod() {return reinterpret_cast<ArtMethod*>(GetField64(ArtMethodOffset())); }@@art/mirror/abstract_method.h static MemberOffset ArtMethodOffset() {return MemberOffset(OFFSETOF_MEMBER(AbstractMethod, art_method_));}從源碼可以看出C層在獲取ArtMethod的地址,實(shí)際上就是把AbstractMethod的artMethod強(qiáng)制轉(zhuǎn)換成了ArtMethod*指針,及我們在Java拿到的artMethod就是c層ArtMethod的實(shí)際地址。是不是很簡單。
參考
- Unsafe
- AndFix
總結(jié)
以上是生活随笔為你收集整理的JAndFix: 基于Java实现的Android实时热修复方案的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android Activity中加入V
- 下一篇: 关于Android中工作者线程的思考