前言css
前段時間,Android平臺上涌現了一系列熱修復方案,如阿里的Andfix、微信的Tinker、QQ空間的Nuva、手Q的QFix等等。java
其中,Andfix的即時生效使人印象深入,它稍顯另類,並不須要從新啓動,而是在加載補丁後直接對方法進行替換就能夠完成修復,然而它的使用限制也遭遇到更多的質疑。android
咱們也對代碼的native替換原理從新進行了深刻思考,從克服其限制和兼容性入手,以一種更加優雅的替換思路,實現了即時生效的代碼熱修復。api
咱們先來看一下,爲什麼惟獨Andfix可以作到即時生效呢?數組
緣由是這樣的,在app運行到一半的時候,全部須要發生變動的Class已經被加載過了,在Android上是沒法對一個Class進行卸載的。而騰訊系的方案,都是讓Classloader去加載新的類。若是不重啓,原來的類還在虛擬機中,就沒法加載新類。所以,只有在下次重啓的時候,在還沒走到業務邏輯以前搶先加載補丁中的新類,這樣後續訪問這個類時,就會Resolve爲新的類。從而達到熱修復的目的。安全
Andfix採用的方法是,在已經加載了的類中直接在native層替換掉原有方法,是在原來類的基礎上進行修改的。咱們這就來看一下Andfix的具體實現。微信
其核心在於replaceMethod函數數據結構
@AndFix/src/com/alipay/euler/andfix/AndFix.java private static native void replaceMethod(Method src, Method dest);
這是一個native方法,它的參數是在Java層經過反射機制獲得的Method對象所對應的jobject。src對應的是須要被替換的原有方法。而dest對應的就是新方法,新方法存在於補丁包的新類中,也就是補丁方法。app
@AndFix/jni/andfix.cpp static void replaceMethod(JNIEnv* env, jclass clazz, jobject src, jobject dest) { if (isArt) { art_replaceMethod(env, src, dest); } else { dalvik_replaceMethod(env, src, dest); } }
Android的java運行環境,在4.4如下用的是dalvik虛擬機,而在4.4以上用的是art虛擬機。框架
@AndFix/jni/art/art_method_replace.cpp
extern void __attribute__ ((visibility ("hidden"))) art_replaceMethod( JNIEnv* env, jobject src, jobject dest) { if (apilevel > 23) { replace_7_0(env, src, dest); } else if (apilevel > 22) { replace_6_0(env, src, dest); } else if (apilevel > 21) { replace_5_1(env, src, dest); } else if (apilevel > 19) { replace_5_0(env, src, dest); }else{ replace_4_4(env, src, dest); } }
咱們以art爲例,對於不一樣Android版本的art,底層Java對象的數據結構是不一樣的,於是會進一步區分不一樣的替換函數,這裏咱們以Android 6.0爲例,對應的就是replace_6_0
。
@AndFix/jni/art/art_method_replace_6_0.cpp void replace_6_0(JNIEnv* env, jobject src, jobject dest) { // %% 經過Method對象獲得底層Java函數對應ArtMethod的真實地址。 art::mirror::ArtMethod* smeth = (art::mirror::ArtMethod*) env->FromReflectedMethod(src); art::mirror::ArtMethod* dmeth = (art::mirror::ArtMethod*) env->FromReflectedMethod(dest); ... ... // %% 把舊函數的全部成員變量都替換爲新函數的。 smeth->declaring_class_ = dmeth->declaring_class_; smeth->dex_cache_resolved_methods_ = dmeth->dex_cache_resolved_methods_; smeth->dex_cache_resolved_types_ = dmeth->dex_cache_resolved_types_; smeth->access_flags_ = dmeth->access_flags_; smeth->dex_code_item_offset_ = dmeth->dex_code_item_offset_; smeth->dex_method_index_ = dmeth->dex_method_index_; smeth->method_index_ = dmeth->method_index_; smeth->ptr_sized_fields_.entry_point_from_interpreter_ = dmeth->ptr_sized_fields_.entry_point_from_interpreter_; smeth->ptr_sized_fields_.entry_point_from_jni_ = dmeth->ptr_sized_fields_.entry_point_from_jni_; smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_ = dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_; LOGD("replace_6_0: %d , %d", smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_, dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_); }
每個Java方法在art中都對應着一個ArtMethod,ArtMethod記錄了這個Java方法的全部信息,包括所屬類、訪問權限、代碼執行地址等等。
經過env->FromReflectedMethod
,能夠由Method對象獲得這個方法對應的ArtMethod的真正起始地址。而後就能夠把它強轉爲ArtMethod指針,從而對其全部成員進行修改。
這樣所有替換完以後就完成了熱修復邏輯。之後調用這個方法時就會直接走到新方法的實現中了。
爲何這樣替換完就能夠實現熱修復呢?這須要從虛擬機調用方法的原理提及。
在Android 6.0,art虛擬機中ArtMethod的結構是這個樣子的:
@art/runtime/art_method.h
class 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_了,從名字能夠看出來,他們就是方法的執行入口。咱們知道,Java代碼在Android中會被編譯爲Dex Code。
art中能夠採用解釋模式或者AOT機器碼模式執行。
解釋模式,就是取出Dex Code,逐條解釋執行就好了。若是方法的調用者是以解釋模式運行的,在調用這個方法時,就會取得這個方法的entry_point_from_interpreter_,而後跳轉過去執行。
而若是是AOT的方式,就會先預編譯好Dex Code對應的機器碼,而後運行期直接執行機器碼就好了,不須要一條條地解釋執行Dex Code。若是方法的調用者是以AOT機器碼方式執行的,在調用這個方法時,就是跳轉到entry_point_from_quick_compiled_code_執行。
那咱們是否是隻須要替換這幾個entry_point_*入口地址就可以實現方法替換了呢?
並無這麼簡單。由於不管是解釋模式或是AOT機器碼模式,在運行期間還會須要用到ArtMethod裏面的其餘成員字段。
就以AOT機器碼模式爲例,雖然Dex Code被編譯成了機器碼。可是機器碼並非能夠脫離虛擬機而單獨運行的,以這段簡單的代碼爲例:
public class MainActivity extends Activity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } ... ...
編譯爲AOT機器碼後,是這樣的:
7: void com.patch.demo.MainActivity.onCreate(android.os.Bundle) (dex_method_idx=20639) DEX CODE: 0x0000: 6f20 4600 1000 | invoke-super {v0, v1}, void android.app.Activity.onCreate(android.os.Bundle) // method@70 0x0003: 0e00 | return-void CODE: (code_offset=0x006fdbac size_offset=0x006fdba8 size=96) ... ... 0x006fdbe0: f94003e0 ldr x0, [sp] ;x0 = MainActivity.onCreate對應的ArtMethod指針 0x006fdbe4: b9400400 ldr w0, [x0, #4] ;w0 = [x0 + 4] = dex_cache_resolved_methods_字段 0x006fdbe8: f9412000 ldr x0, [x0, #576] ;x0 = [x0 + 576] = dex_cache_resolved_methods_數組的第72(=576/8)個元素,即對應Activity.onCreate的ArtMethod指針 0x006fdbec: f940181e ldr lr, [x0, #48] ;lr = [x0 + 48] = Activity.onCreate的ArtMethod成員的entry_point_from_quick_compiled_code_執行入口點 0x006fdbf0: d63f03c0 blr lr ;調用Activity.onCreate ... ...
這裏面我去掉了一些校驗之類的無關代碼,能夠很清楚看到,在調用一個方法時,取得了ArtMethod中的dex_cache_resolved_methods_,這是一個存放ArtMethod*的指針數組,經過它就能夠訪問到這個Method所在Dex中全部的Method所對應的ArtMethod*。
Activity.onCreate的方法索引是70,因爲是64位系統,所以每一個指針的大小爲8字節,又因爲ArtMethod*元素是從這個數組的第0x2個位置開始存放的,所以偏移(70 + 2) * 8 = 576的位置正是Activity.onCreate的ArtMethod指針。
這是一個比較簡單的例子,而在實際代碼中,有許多更爲複雜的調用狀況。不少狀況下還須要用到dex_code_item_offset_等字段。由此能夠看出,AOT機器碼的執行過程,仍是會有對於虛擬機以及ArtMethod其餘成員字段的依賴。
所以,當把一箇舊方法的全部成員字段換成都新方法後,執行時全部數據就能夠保持和新方法的一致。這樣在全部執行到舊方法的地方,會取得新方法的執行入口、所屬class、方法索引號以及所屬dex信息,而後像調用舊方法同樣順滑地執行到新方法的邏輯。
然而,目前市面上幾乎全部的native替換方案,好比Andfix和另外一種Hook框架Legend,都是寫死了ArtMethod結構體,這會帶來巨大的兼容性問題。
從剛纔的分析能夠看到,雖然Andfix是把底層結構強轉爲了art::mirror::ArtMethod,但這裏的art::mirror::ArtMethod並不是等同於app運行時所在設備虛擬機底層的art::mirror::ArtMethod,而是Andfix本身構造的art::mirror::ArtMethod。
@AndFix/jni/art/art_6_0.h class ArtMethod { public: // Field order required by test "ValidateFieldOrderOfJavaCppUnionClasses". // The class we are a part of. uint32_t declaring_class_; // Short cuts to declaring_class_->dex_cache_ member for fast compiled code access. uint32_t dex_cache_resolved_methods_; // Short cuts to declaring_class_->dex_cache_ member for fast compiled code access. uint32_t 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 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_; };
咱們再來回顧一下Android開源代碼裏面art虛擬機裏的ArtMethod:
@art/runtime/art_method.h
class 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_; ... ... }
能夠看到,ArtMethod結構裏的各個成員的大小是和AOSP開源代碼裏徹底一致的。這是因爲Android源碼是公開的,Andfix裏面的這個ArtMethod天然是遵守android虛擬機art源碼裏面的ArtMethod構建的。
可是,因爲Android是開源的,各個手機廠商均可以對代碼進行改造,而Andfix裏ArtMethod的結構是根據公開的Android源碼中的結構寫死的。若是某個廠商對這個ArtMethod結構體進行了修改,就和原先開源代碼裏的結構不一致,那麼在這個修改過了的設備上,替換機制就會出問題。
好比,在Andfix替換declaring_class_
的地方,
smeth->declaring_class_ = dmeth->declaring_class_;
因爲declaring_class_
是andfix裏ArtMethod的第一個成員,所以它和如下這行代碼等價:
*(uint32_t*) (smeth + 0) = *(uint32_t*) (dmeth + 0)
若是手機廠商在ArtMethod結構體的declaring_class_
前面添加了一個字段additional_
,那麼,additional_就成爲了ArtMethod的第一個成員,因此smeth + 0這個位置在這臺設備上實際就變成了additional_
,而再也不是declaring_class_
字段。因此這行代碼的真正含義就變成了:
smeth->additional_ = dmeth->additional_;
這樣就和原先替換declaring_class_
的邏輯不一致,從而沒法正常執行熱修復邏輯。
這也正是Andfix不支持不少機型的緣由,很大的可能,就是由於這些機型修改了底層的虛擬機結構。
知道了native替換方式兼容性問題的緣由,咱們是否有辦法尋求一種新的方式,不依賴於ROM底層方法結構的實現而達到替換效果呢?
咱們發現,這樣native層面替換思路,其實就是替換ArtMethod的全部成員。那麼,咱們並不須要構造出ArtMethod具體的各個成員字段,只要把ArtMethod的做爲總體進行替換,這樣不就能夠了嗎?
也就是把原先這樣的逐一替換
變成了這樣的總體替換
所以Andfix這一系列繁瑣的替換:
// %% 把舊函數的全部成員變量都替換爲新函數的。 smeth->declaring_class_ = dmeth->declaring_class_; smeth->dex_cache_resolved_methods_ = dmeth->dex_cache_resolved_methods_; smeth->dex_cache_resolved_types_ = dmeth->dex_cache_resolved_types_; smeth->access_flags_ = dmeth->access_flags_; smeth->dex_code_item_offset_ = dmeth->dex_code_item_offset_; smeth->dex_method_index_ = dmeth->dex_method_index_; smeth->method_index_ = dmeth->method_index_; ... ...
其實能夠濃縮爲:
memcpy(smeth, dmeth, sizeof(ArtMethod));
就是這樣,一句話就能取代上面一堆代碼,這正是咱們深刻理解替換機制的本質以後研發出的新替換方案。
剛纔提到過,不一樣的手機廠商均可以對底層的ArtMethod進行任意修改,但即便他們把ArtMethod改得六親不認,只要我像這樣把整個ArtMethod結構體完整替換了,就可以把全部舊方法成員自動對應地換成新方法的成員。
但這其中最關鍵的地方,在於sizeof(ArtMethod)。若是size計算有誤差,致使部分紅員沒有被替換,或者替換區域超出了邊界,都會致使嚴重的問題。
對於ROM開發者而言,是在art源代碼裏面,因此一個簡單的sizeof(ArtMethod)
就好了,由於這是在編譯期就能夠決定的。
但咱們是上層開發者,app會被下發給各式各樣的Android設備,因此咱們是須要在運行時動態地獲得app所運行設備上面的底層ArtMethod大小的,這就沒那麼簡單了。
想要忽略ArtMethod的具體結構成員直接取得其size的精確值,咱們仍是須要從虛擬機的源碼入手,從底層的數據結構及排列特色探尋答案。
在art裏面,初始化一個類的時候會給這個類的全部方法分配空間,咱們能夠看到這個分配空間的地方:
@android-6.0.1_r62/art/runtime/class_linker.cc void ClassLinker::LoadClassMembers(Thread* self, const DexFile& dex_file, const uint8_t* class_data, Handle<mirror::Class> klass, const OatFile::OatClass* oat_class) { ... ... ArtMethod* const direct_methods = (it.NumDirectMethods() != 0) ? AllocArtMethodArray(self, it.NumDirectMethods()) : nullptr; ArtMethod* const virtual_methods = (it.NumVirtualMethods() != 0) ? AllocArtMethodArray(self, it.NumVirtualMethods()) : nullptr; ... ...
類的方法有direct方法和virtual方法。direct方法包含static方法和全部不可繼承的對象方法。而virtual方法就是全部能夠繼承的對象方法了。
AllocArtMethodArray函數分配了他們的方法所在區域。
@android-6.0.1_r62/art/runtime/class_linker.cc ArtMethod* ClassLinker::AllocArtMethodArray(Thread* self, size_t length) { const size_t method_size = ArtMethod::ObjectSize(image_pointer_size_); uintptr_t ptr = reinterpret_cast<uintptr_t>( Runtime::Current()->GetLinearAlloc()->Alloc(self, method_size * length)); CHECK_NE(ptr, 0u); for (size_t i = 0; i < length; ++i) { new(reinterpret_cast<void*>(ptr + i * method_size)) ArtMethod; } return reinterpret_cast<ArtMethod*>(ptr); }
能夠看到,ptr是這個方法數組的指針,而方法是一個接一個緊密地new出來排列在這個方法數組中的。這時只是分配出空間,還沒填入真正的ArtMethod的各個成員值,不過這並不影響咱們觀察ArtMethod的空間結構。
正是這裏給了咱們啓示,ArtMethod們是緊密排列的,因此一個ArtMethod的大小,不就是相鄰兩個方法所對應的ArtMethod的起始地址的差值嗎?
正是如此。咱們就從這個排列特色入手,本身構造一個類,以一種巧妙的方式獲取到這個差值。
public class NativeStructsModel { final public static void f1() {} final public static void f2() {} }
因爲f1和f2都是static方法,因此都屬於direct ArtMethod Array。因爲NativeStructsModel類中只存在這兩個方法,所以它們確定是相鄰的。
那麼咱們就能夠在JNI層取得它們地址的差值:
size_t firMid = (size_t) env->GetStaticMethodID(nativeStructsModelClazz, "f1", "()V"); size_t secMid = (size_t) env->GetStaticMethodID(nativeStructsModelClazz, "f2", "()V"); size_t methSize = secMid - firMid;
而後,就以這個methSize
做爲sizeof(ArtMethod)
,代入以前的代碼。
memcpy(smeth, dmeth, methSize);
問題就迎刃而解了。
值得一提的是,因爲忽略了底層ArtMethod結構的差別,對於全部的Android版本都再也不須要區分,而統一以memcpy
實現便可,代碼量大大減小。即便之後的Android版本不斷修改ArtMethod的成員,只要保證ArtMethod數組還是以線性結構排列,就能直接適用於未來的Android 8.0、9.0等新版本,無需再針對新的系統版本進行適配了。事實也證實確實如此,當咱們拿到Google剛發不久的Android O(8.0)開發者預覽版的系統時,hotfix demo直接就能順利地加載補丁跑起來了,咱們並無作任何適配工做,魯棒性極好。
看到這裏,你可能會有疑惑:咱們只是替換了ArtMethod的內容,但新替換的方法的所屬類,和原先方法的所屬類,是不一樣的類,被替換的方法有權限訪問這個類的其餘private方法嗎?
以這段簡單的代碼爲例
public class Demo { Demo() { func(); } private void func() { } }
Demo構造函數調用私有函數func
所對應的Dex Code和Native Code爲
void com.patch.demo.Demo.<init>() (dex_method_idx=20628) DEX CODE: ... ... 0x0003: 7010 9550 0000 | invoke-direct {v0}, void com.patch.demo.Demo.func() // method@20629 ... ... CODE: (code_offset=0x006fd86c size_offset=0x006fd868 size=140)... ... ... 0x006fd8c4: f94003e0 ldr x0, [sp] ; x0 = <init>的ArtMethod* 0x006fd8c8: b9400400 ldr w0, [x0, #4] ; w0 = dex_cache_resolved_methods_ 0x006fd8cc: d2909710 mov x16, #0x84b8 ; x16 = 0x84b8 0x006fd8d0: f2a00050 movk x16, #0x2, lsl #16 ; x16 = 0x84b8 + 0x20000 = 0x284b8 = (20629 + 2) * 8, ; 也就是Demo.func的ArtMethod*相對於表頭dex_cache_resolved_methods_的偏移。 0x006fd8d4: f8706800 ldr x0, [x0, x16] ; 獲得Demo.func的ArtMethod* 0x006fd8d8: f940181e ldr lr, [x0, #48] ; 取得其entry_point_from_quick_compiled_code_ 0x006fd8dc: d63f03c0 blr lr ; 跳轉執行 ... ...
這個調用邏輯和以前Activity的例子大同小異,須要注意的地方是,在構造函數調用同一個類下的私有方法func
時,沒有作任何權限檢查。也就是說,這時即便我把func
方法的偷樑換柱,也能直接跳過去正常執行而不會報錯。
能夠推測,在dex2oat生成AOT機器碼時是有作一些檢查和優化的,因爲在dex2oat編譯機器碼時確認了兩個方法同屬一個類,因此機器碼中就不存在權限檢查的相關代碼。
可是,並不是全部方法均可以這麼順利地進行訪問的。咱們發現補丁中的類在訪問同包名下的類時,會報出訪問權限異常:
Caused by: java.lang.IllegalAccessError:
Method 'void com.patch.demo.BaseBug.test()' is inaccessible to class 'com.patch.demo.MyClass' (declaration of 'com.patch.demo.MyClass' appears in /data/user/0/com.patch.demo/files/baichuan.fix/patch/patch.jar)
雖然com.patch.demo.BaseBug
和com.patch.demo.MyClass
是同一個包com.patch.demo
下面的,可是因爲咱們替換了com.patch.demo.BaseBug.test
,而這個替換了的BaseBug.test
是從補丁包的Classloader加載的,與原先的base包就不是同一個Classloader了,這樣就致使兩個類沒法被判別爲同包名。具體的校驗邏輯是在虛擬機代碼的Class::IsInSamePackage
中:
android-6.0.1_r62/art/runtime/mirror/class.cc bool Class::IsInSamePackage(Class* that) { Class* klass1 = this; Class* klass2 = that; if (klass1 == klass2) { return true; } // Class loaders must match. if (klass1->GetClassLoader() != klass2->GetClassLoader()) { return false; } // Arrays are in the same package when their element classes are. while (klass1->IsArrayClass()) { klass1 = klass1->GetComponentType(); } while (klass2->IsArrayClass()) { klass2 = klass2->GetComponentType(); } // trivial check again for array types if (klass1 == klass2) { return true; } // Compare the package part of the descriptor string. std::string temp1, temp2; return IsInSamePackage(klass1->GetDescriptor(&temp1), klass2->GetDescriptor(&temp2)); }
關鍵點在於,Class loaders must match這行註釋。
知道了緣由就好解決了,咱們只要設置新類的Classloader爲原來類就能夠了。而這一步一樣不須要在JNI層構造底層的結構,只須要經過反射進行設置。這樣仍舊可以保證良好的兼容性。
實現代碼以下:
Field classLoaderField = Class.class.getDeclaredField("classLoader"); classLoaderField.setAccessible(true); classLoaderField.set(newClass, oldClass.getClassLoader());
這樣就解決了同包名下的訪問權限問題。
當一個非靜態方法被熱替換後,在反射調用這個方法時,會拋出異常。
好比下面這個例子:
// BaseBug.test方法已經被熱替換了。 ... ... BaseBug bb = new BaseBug(); Method testMeth = BaseBug.class.getDeclaredMethod("test"); testMeth.invoke(bb);
invoke的時候就會報:
Caused by: java.lang.IllegalArgumentException: Expected receiver of type com.patch.demo.BaseBug, but got com.patch.demo.BaseBug
這裏面,expected receiver的BaseBug,和got到的BaseBug,雖然都叫com.patch.demo.BaseBug,但倒是不一樣的類。
前者是被熱替換的方法所屬的類,因爲咱們把它的ArtMethod的declaring_class_替換了,所以就是新的補丁類。然後者做爲被調用的實例對象bb的所屬類,是原有的BaseBug。二者是不一樣的。
在反射invoke這個方法時,在底層會調用到InvokeMethod:
jobject InvokeMethod(const ScopedObjectAccessAlreadyRunnable& soa, jobject javaMethod, jobject javaReceiver, jobject javaArgs, size_t num_frames) { ... ... if (!VerifyObjectIsClass(receiver, declaring_class)) { return nullptr; } ... ...
這裏面會調用VerifyObjectIsClass函數作驗證。
inline bool VerifyObjectIsClass(mirror::Object* o, mirror::Class* c) { if (UNLIKELY(o == nullptr)) { ThrowNullPointerException("null receiver"); return false; } else if (UNLIKELY(!o->InstanceOf(c))) { InvalidReceiverError(o, c); return false; } return true; }
o表示Method.invoke傳入的第一個參數,也就是做用的對象。
c表示ArtMethod所屬的Class。
所以,只有o是c的一個實例纔可以經過驗證,才能繼續執行後面的反射調用流程。
由此可知,這種熱替換方式所替換的非靜態方法,在進行反射調用時,因爲VerifyObjectIsClass時舊類和新類不匹配,就會致使校驗不經過,從而拋出上面那個異常。
那爲何方法是非靜態纔有這個問題呢?由於若是是靜態方法,是在類的級別直接進行調用的,就不須要接收對象實例做爲參數。因此就沒有這方面的檢查了。
對於這種反射調用非靜態方法的問題,咱們會採用另外一種冷啓動機制對付,本文在最後會說明如何解決。
除了反射的問題,像本方案以及Andfix這樣直接在運行期修改底層結構的熱修復,都存在着一個限制,那就是隻能支持方法的替換。而對於補丁類裏面存在方法增長和減小,以及成員字段的增長和減小的狀況,都是不適用的。
緣由是這樣的,一旦補丁類中出現了方法的增長和減小,就會致使這個類以及整個Dex的方法數的變化。方法數的變化伴隨着方法索引的變化,這樣在訪問方法時就沒法正常地索引到正確的方法了。
而若是字段發生了增長和減小,和方法變化的狀況同樣,全部字段的索引都會發生變化。而且更嚴重的問題是,若是在程序運行中間某個類忽然增長了一個字段,那麼對於原先已經產生的這個類的實例,它們仍是原來的結構,這是沒法改變的。而新方法使用到這些老的實例對象時,訪問新增字段就會產生不可預期的結果。
不過新增一個完整的、原先包裏面不存在的新類是能夠的,這個不受限制。
總之,只有兩種狀況是不適用的:1).引發原有了類中發生結構變化的修改,2).修復了的非靜態方法會被反射調用,而對於其餘狀況,這種方式的熱修復均可以任意使用。
雖然有着一些使用限制,但一旦知足使用條件,這種熱修復方式是十分出衆的,它補丁小,加載迅速,可以實時生效無需從新啓動app,而且具備着完美的設備兼容性。對於較小程度的修復再適合不過了。
本修復方案將最早在阿里Hotfix最新版本(Sophix)上應用,由手機淘寶技術團隊與阿里雲聯合發佈。
Sophix提供了一套更加完美的客戶端服務端一體的熱更新方案。針對小修改能夠採用本文這種即時生效的熱修復,而且能夠結合資源修復,作到資源和代碼的即時生效。
而若是觸及了本文提到的熱替換使用限制,對於比較大的代碼改動以及被修復方法反射調用狀況,Sophix也提供了另外一種完整代碼修復機制,不過是須要app從新冷啓動,來發揮其更加完善的修復及更新功能。從而能夠作到無感知的應用更新。
而且Sophix作到了圖形界面一鍵打包、加密傳輸、簽名校驗和服務端控制發佈與灰度功能,讓你用最少的時間實現最強大可靠的全方位熱更新。
一張表格來講明一下各個版本熱修復的差異:
方案對比 | Andfix開源版本 | 阿里Hotfix 1.X | 阿里Hotfix最新版(Sophix) |
---|---|---|---|
方法替換 | 支持,除部分狀況[0] | 支持,除部分狀況 | 所有支持 |
方法增長減小 | 不支持 | 不支持 | 以冷啓動方式支持[1] |
方法反射調用 | 只支持靜態方法 | 只支持靜態方法 | 以冷啓動方式支持 |
即時生效 | 支持 | 支持 | 視狀況支持[2] |
多DEX | 不支持 | 支持 | 支持 |
資源更新 | 不支持 | 不支持 | 支持 |
so庫更新 | 不支持 | 不支持 | 支持 |
Android版本 | 支持2.3~7.0 | 支持2.3~6.0 | 所有支持包含7.0以上 |
已有機型 | 大部分支持[3] | 大部分支持 | 所有支持 |
安全機制 | 無 | 加密傳輸及簽名校驗 | 加密傳輸及簽名校驗 |
性能損耗 | 低,幾乎無損耗 | 低,幾乎無損耗 | 低,僅冷啓動狀況下有些損耗 |
生成補丁 | 繁瑣,命令行操做 | 繁瑣,命令行操做 | 便捷,圖形化界面 |
補丁大小 | 不大,僅變更的類 | 小,僅變更的方法 | 不大,僅變更的資源和代碼[4] |
服務端支持 | 無 | 支持服務端控制[5] | 支持服務端控制 |
說明:
[0] 部分狀況指的是構造方法、參數數目大於8或者參數包括long,double,float基本類型的方法。
[1] 冷啓動方式,指的是須要重啓app在下次啓動時才能生效。
[2] 對於Andfix及Hotfix 1.X可以支持的代碼變更狀況,都能作到即時生效。而對於Andfix及Hotfix 1.X不支持的代碼變更狀況,會走冷啓動方式,此時就沒法作到即時生效。
[3] Hotfix 1.X已經支持絕大部分主流手機,只是在X86設備以及修改了虛擬機底層結構的ROM上不支持。
[4] 因爲支持了資源和庫,若是有這些方面的更新,就會致使的補丁變大一些,這個是很正常的。而且因爲只包含差別的部分,因此補丁已是最大程度的小了。
[5] 提供服務端的補丁發佈和停發、版本控制和灰度功能,存儲開發者上傳的補丁包。
1.代碼修復
1.1 即時生效:底層替代類中的老代碼,而且無視底層的具體結構。
1.2 重啓生效:基於類加載機制,從新編排了包中dex的順序。
2.資源修復
2.1 傳統的資源修復是基於InstantRun的原理,就是構造一個新的AssetManager,將新的資源進行addAssetPath,而後經過反射替換掉系統中的原理的AssetManager的引用。
2.2 阿里採用的是直接將一個比系統資源包的packageId 0x7F小的packageId爲0x66的資源addAssetPath到原來的AssetManager對象上便可,這個補丁資源包只包含新添加,和已修改的。
3.so修復
本質是對native方法的修復和替換,阿里採用的是相似類修復反射注入的方式,把補丁so路徑插入到nativeLibrary數組的最前面。
1. 底層熱替換原理
1.1 Andfix 原理:經過jni的replaceMethod(Method src ,Method des )->經過 env的FromReflectMethod獲得ArtMethod地址,轉爲ArtMethod指針->挨個替換ArtMethod的中字段.
1.2 虛擬機調用方法的原理 : 最終ArtMethod中的字段(例如entry_point_from_interpreter)找到最終要執行的方法的入口地址,art能夠採用解釋模式或者AOT機器碼模式執行。
1.3 Andfix原理兼容性的根源 : ArtMethod的結構廠商能夠本身改變,就會致使替換字段信息不是代碼中指定的信息,致使替換錯亂
1.4 突破底層ArtMethod結構的差別 : 將ArtMethod總體替換,阿里的核心方法是memcry(smeth,dmeth,sizeOf(ArtMethod))。這裏面的關鍵是sizeOf(ArtMethod)的實現,其原理是ArtMethod的存儲接口是線性的,經過兩個ArtMethod的地址差就能夠。這種方式的適配不受系統的影響,穩定且兼容。
1.5 訪問權限的問題 :
* 方法時訪問權限 : 機器碼中不存在檢查權限的相關代碼
* 同包名下訪問權限的問題 : 因爲補丁包的ClassLoader與原來的ClassLoader不一致,致使虛擬機代碼的Class::IsInSamePackage校驗失敗。解決方案就是經過反射讓補丁包的ClassLoader爲系統原來的ClassLoader便可。
* 被反射調用的方法問題 : 因爲ArtMethod中的declaring_class_被替換成了新的類,而反射獲得的仍是原來的老類,這會致使invoke時VerifyObjectClass()方法失敗,而直接報錯。因此這種熱修復方案不能修復這種方法。
1.6 即時生效的限制:
引發類中發生結構變化的修改 : 由於一旦引發修改ArtMethod的位置將發生變化,就找不到地址了。
修復了的非靜態方法被反射調用。
2. java中的祕密
2.1 內部編譯類
* 內部類在編譯器會被編譯爲跟外部類同樣的類
* 靜態內部類與非靜態內部類,在smali中非靜態內部類會自動合成this$0 域標示的是外部類的引用。
* 外部類爲了訪問內部類(或內部類訪問外部類)的私有域,編譯期間會自動爲內部類(或外部類)生成access&XXX方法。
* 熱修復替換時,要避免生成access&XXX方法,就要求內/外部類不能存在private的method/field。
2.2 匿名內部類
* 匿名內部類的名稱格式通常爲外部類&number,number根據匿名內部類出現的順序累加記名。
* 若是在以前增長一個匿名內部類 則會致使原來的匿名內部類名稱不對應。也就沒法使用熱修復。
* 應當極力避免插入新匿名內部類,特別是向前插。
2.3 域編譯
* 熱替換不支持 clint方法
* 靜態域和靜態代碼塊在clint方法中
* 非靜態在init方法中
* 靜態域和靜態代碼塊不支持熱替換
2.4 final static 域
* final static 原始類型和字符串在initSField而不是在clint中
* final static 引用類型在 clint方法中初始化
* 優化時final static 對於原始類型和字符串有用,引用類型其實沒有用。
2.5 方法編譯
* 混淆可能致使方法的內聯和裁剪
* 被內聯:方法沒被用過,方法只有一行代碼,方法只被一個地方引用過。
* 被裁剪:方法中有參數沒被使用。
* 熱替換解決方法:在混淆是加上配置 -dontoptimize
2.6 switch case 語句編譯
* 連續幾個相近的值會被編譯爲packed-switch指令,中間差值用pswitch-0補齊。
* 不連續邊被編譯爲sparse-switch指令
* 熱替換方案:資源id爲const final static 會被編譯爲packed-switch指令,會存在資源id替換不徹底的問題,解決方案就是修改smali反編譯流程,碰到packed-switch指令強替換爲sparse-switch指令,:pswitch-N標籤強改成sswitch-N標籤,而後作資源id的強替換,在回編譯smali爲dex。
2.7 泛型編譯
* 泛型在編譯器中實現,虛擬機無感知
* 泛型類型擦除:編譯器在編譯期間將泛型轉爲目標類型的字節碼,對於虛擬機來講獲得的是目標類型的字節碼文件,無感知泛型。
* 泛型與多態衝突的原理及方案:
*類型擦除後 原來的set(T t)的字節碼會是set(Object t) 而其子類爲set(Number t),從重寫的定義上來看這不是重寫而是重載。這也就致使泛型和多態有衝突了
*而實際是能夠重寫的,其本質緣由是JVM採用了bridge方法。子類真正重寫父類方法是bridge方法,而在bridge方法中調用了子類的方法而已。@override只是個假象。
*泛型不須要強制類型轉換的緣由是:編譯器若是返現有一個變量申明加上了泛型的話,編譯器會自動加上chceck-cast類型轉換。
2.8 Lambda 表達
* Lambda 會被;;其內部this指的是外部類對象,這點區別於內部類的this。
* 函數式接口 : 只有一個方法的接口
* 函數式接口調用時,最終會增長一個輔助方法。不能走熱替換
* 修改函數式接口內部邏輯能夠走熱替換
2.9 訪問權限檢查對熱替換的影響
*補丁類若是引用了非public類,最終會拋dvmThrowException
2.10 Clint方法
* 不支持clint方法的熱替換
3 冷啓動方案
3.1 傳統實現方式的利弊
* QQ控件的插莊方案:
原理:單獨放一個類在dex中,讓其它類調用,防止打上CLASS_ISPREVERIFIED標誌,再加載補丁dex獲得dexFile對象做爲參數構建一個Element對象插入到dex-Elements數組的前面。
缺點:Dalvik下影響類加載性能,Art下類地址寫死,致使必須包含父類或引用,最後致使補丁包很大。
*Tinker方案:
原理: 提供dex差量包,總體替換dex的方案。差量的方式給出patch.dexm,而後將patch.dex和應用的classes.dex合併成一個完整的dex,
完整的dex加載獲得的dexFile對象做爲參數構建一個Elements對象而後總體替換掉舊的dex-Elements數組。
缺點: dex合併內存消耗在Vm heap上,容易OOM,最後致使dex合併失敗
3.2 插樁實現的來龍去脈
默認一個dex時全部類會打上CLASS_ISPREVERIFIED標誌,新的補丁類不在原dex中時,被調用會報dvmThrowllegalAccessError。一個單獨的輔助類放到一個單獨的dex中,原dex的全部類的構造函數都引用這個類,dexopt時原Dex全部類不會被打上CLASS_ISPREVERIFIED這個標誌。
3.3 插樁致使類加載性能影響
採用插樁,致使全部類都是非preverify,這就使得dexopt和load class時頻繁的verify和optimize。當類不少時這個操做會至關耗時,致使啓動時長時間白屏。
3.4 避免插樁的QFix方案
在dexopt後進行檢查繞過,會存在潛在的Bug
3.5 Art下冷啓動實現 將補丁直接命名爲classes.dex 將原來的一次命名爲classes1.dex …classes2.dex…等。而後一塊兒打包爲一個apk。而後DexFile.loadDex獲得DexFile對象,最後把該DexFile對象總體替換舊的dexElements數組