1.底層替換原理
在各種Android熱修復方案中,Andfix的即時生效特徵令人印象深刻,它並不需要重新啓動App,而是在加載補丁後直接對方法進行替換就可以完成修復,然而它的使用限制也遭到了更多的質疑。
1.1 Andfix回顧
我們先來看一下,爲何唯獨Andfix能夠做到即時生效呢?
原因是這樣的,在App啓動到一半的時候,所有需要發生變更的分類已經被加載過了,在Android系統中是無法對一個分類進行卸載的。而騰訊系的方案是讓Classloader去加載新的類,如果不重啓App,原有的類還在虛擬機中,就無法加載新類。因此,只有在下次App重啓的時候,在還沒有運行到業務邏輯之前搶先加載補丁中的新類,這樣在後續訪問這個類時,就會解析爲新的類。從而達到熱修復的目的。
Andfix採用的方法是直接在已經加載的類中native層替換掉原有的方法,是在原有類的基礎上進行修改的。來看下 Andfix的具體實現,其核心在於 replaceMethod()
:
// AndFix/src/com/alipay/euler/andfix/AndFix.java
private static native void replaceMethod(Method dest, Method src);
這是一個native方法, 它的參數是在 Java層通過反射機制得到的 Method
, src對應被替換的原有方法,而dest對應的就是新方法,新方法存在於補丁包的新類中,也就是補丁方法。
// 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版本做了判斷,4.4以下版本用的是Dalvik虛擬機,而在 4.4以上則用的是ART虛擬機。
在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) {
art::mirror::ArtMethod* smeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(src); // 1
art::mirror::ArtMethod* dmeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(dest); // 2
...
// 3
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_ | 0x0001;
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_);
}
註釋1、2:通過Method對象得到底層Java函數對應ArtMethod的真實地址。
註釋3:把就函數的所有成員變量都替換爲新的。(通過ArtMehtod結構體)(注意注意!這裏相當於是替換了原結構體中的字段信息)
每一個Java方法在Art虛擬機中都對應着一個 ArtMethod
,ArtMethod記錄了這個Java方法的所有信息,包括所屬類、訪問權限、代碼執行地址等。
通過 env->FromReflectedMethod
,可以由 Method對象得到這個方法所對應的 ArtMethod的真正其實地址,然後就可以把它強制轉化爲ArtMethod指針,從而對其包含的所有成員進行修改。
這樣全部替換完之後就完成了熱修復功能。以後調用方法時就會直接運行到新方法中實現。
1.2 虛擬機調用方法的原理
爲什麼這樣替換完就可以實現熱修復呢?這需要從虛擬機調用方法的原理說起。
在Android6.0版本中,Art虛擬機中的 Art虛擬機中ArtMethod的結構如下:
// art/runtime/art_method.h
class ArtMethod FINAL {
...
protected:
GcRoot<mirror::Class> declaring_class_;
GcRoot<mirror::PointerArray> dex_cache_resolved_methods_;
GcRoot<mirror::ObjectArray<mirror::Class>> dex_cache_resolved_types_;
uint32_t access_flags_;
uint32_t dex_code_item_offset_;
uint32_t dex_method_index_;
uint32_t method_index_;
struct PACKED(4) PtrSizedFields {
void* entry_point_from_interpreter_; // 1
void* entry_point_from_jni_;
void* entry_point_from_quick_compiled_code_; //2
} ptr_sized_fields_;
...
}
在 ArtMethod結構體中,最重要的就是 註釋1和註釋2標註的內容,從名字可以看出來,他們就是方法的執行入口。
我們知道,Java代碼在Android中會被編譯爲 Dex Code。
Art虛擬機中可以採用解釋模式或者 AOT機器碼模式執行 Dex Code
- 解釋模式
就是去除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 saveInstanceState) {
super.onCreate(saveInstanceState);
}
}
編譯爲 AOT機器碼後,是這樣的:
7:void com.rikkatheworld.demo.MainActivity.onCreate(android.os.Bundle) (dex_method_idx)
DEX CODE:
0x0000: 6f20 4600 1000 | invoke-super {v0, v1}, void anroid.App.Activity.onCreate(android.os.Bundle)
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, [w0, #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位系統,因此每個指針的大小爲8kb,又由於ArtMethod*元素時從這個數組的第0×2 個位置開始存放的,因此偏移量爲 (70 + 2) * 8 = 576的位置正是 Activity.onCreate() 方法的 ArtMethod指針。
這只是一個比較簡單的例子,而在實際代碼中,有許多更爲複雜的調用情況,很多情況下還需要調用 dex_code_item_offset_
字段。由此可以看出,AOT機器碼的執行過程,還是會有對虛擬機以及ArtMethod成員字段的依賴。
因此,當把一箇舊方法的所有成員字段都換爲新方法的成員字段後,執行時所有的數據就可以保持和新方法的數據一致。這樣在所有執行到舊方法的地方,會獲取新方法的執行入口、所屬類型、方法索引號以及所屬dex信息,然後像調用舊方法一樣去執行新方法的邏輯。
1.3 兼容性問題的根源
然而,目前市場上幾乎所有的native替換方案,比如 Andfix和其他安全界的Hook方案,ArtMethod結構體的結構都是固定的,這回帶來巨大的兼容性問題。
從剛纔的分析可以看到,雖然Andfix是把底層結構強轉爲 art::mirror::ArtMethod,但這裏的 art::mirror::ArtMethod
並非等同於App運行時所在設備虛擬機底層的 art::mirror::ArtMethod
,而是 Andfix自己構造的 art::mirror::ArtMethod
。
由於 Android的源碼是開源的,所以各個手機廠商都可以對代碼進行改造,而Andfix裏ArtMethod的結構是根據公開的Android源碼中的結構編寫的。如果某個廠商對這個 ArtMethod結構體進行了更改,就和原有的開源代碼裏的結構不一致,那麼在這個修改過ArtMethod結構體的設備上,替換機制就會出現問題。
舉個例子,在Andfix替換 declaring_class_
的地方:
//這裏是設備本身的ArtMethod和AndFix的ArtMethod一樣的情況下
smeth->declaring_class_ = demth->declaring_class;
由於 declaring_class_
是 Andfix裏 ArtMethod的第一個成員,因此它和以下這行代碼等價:
*(uint32_t*) (smth + 0) = *(uint32_t*) (dmeth + 0)
如果某個手機廠商在 ArtMethod結構體的 declaring_class_
前面添加了一個字段 additional_
,那麼 additional_
就成爲了 ArtMethod的第一個成員,所以 smeth + 0 這個位置在這臺設備上實際就變成了 additional_
,所以這行代碼的真正含義就變成了:
//這裏是設備本身的ArtMethod和AndFix的ArtMethod不一樣的情況下
smeth->additional_ = dmeth->additional_;
這樣就和原有的替換邏輯不一致了。
這也正是Andfix不支持所有機型的原因,很大的可能,是因爲這些手機機型修改了底層的虛擬機結構
2.突破底層差異的方法
2.1 突破底層結構差異
知道了 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_;
...
可以縮寫成:
memcpy(smeth, dmeth, sizeof(ArtMethod));
這正是 Sophix爲了解決Andfix的問題,而研發出的新的方案。
剛纔提到過,不同的手機廠商都可以對 ArtMethod
進行替換,只要像這樣吧 ArtMethod
整個結構體完整替換,就能夠把所有舊方法成員自動對應的換成新方法的成員。
但這其中最關鍵的地方在於,sizeof(ArtMethod)
的計算結果,如果計算結果有偏差,導致部分成員沒有被替換,或者替換區域超出了邊界,都會導致嚴重的問題。
對於ROM開發者而言,是在 Art源代碼中開發,所以一個簡單的 sizeof(ArtMethod)就行了,因爲這是在編譯器就可以決定的。
但對於上層開發者,App會被下發給各式各樣的Android設備,所以需要在運行時動態地獲取App運行設備中的底層 ArtMethod大小的,就沒有那麼簡單了。
想要忽略ArtMethod具體結構成員直接獲取其size的精確值,還是需要從虛擬機的源碼入手,從底層的數據結構以及排列特點探尋答案。
在 Art中,初始化一個類的時候會給這個類的所有方法分配內存空間,我們可以看到這個分配內存空間的地方(Android 8.0代碼):
// art/runtime/class_linker.cc
void ClassLinker::LoadClassMembers(Thread* self,
const DexFile& dex_file,
const uint8_t* class_data,
Handle<mirror::Class> klass) {
{
...
klass->SetMethodsPtr(
AllocArtMethodArray(self, allocator, it.NumDirectMethods() + it.NumVirtualMethods()),
it.NumDirectMethods(),
it.NumVirtualMethods());
...
}
類的方法中有 direct方法和 virtual方法。
direct方法包含static方法和所有不可繼承的對象方法。而 virtual方法包含了所有可以繼承的對象方法。
AllocArtMethodArray()
函數用來分配他們的方法所在區域,第三個參數傳的是所有的direct方法和virtual的總數:
// class-linker.cc
LengthPrefixedArray<ArtMethod>* ClassLinker::AllocArtMethodArray(Thread* self,
LinearAlloc* allocator,
size_t length) {
if (length == 0) {
return nullptr;
}
const size_t method_alignment = ArtMethod::Alignment(image_pointer_size_);
const size_t method_size = ArtMethod::Size(image_pointer_size_);
const size_t storage_size =
LengthPrefixedArray<ArtMethod>::ComputeSize(length, method_size, method_alignment);
void* array_storage = allocator->Alloc(self, storage_size);
auto* ret = new (array_storage) LengthPrefixedArray<ArtMethod>(length);
CHECK(ret != nullptr);
// 1
for (size_t i = 0; i < length; ++i) {
new(reinterpret_cast<void*>(&ret->At(i, method_size, method_alignment))) ArtMethod;
}
return ret;
}
註釋1中,建立所有方法總數和的一個循環。每次循環使用 ret->(i, method_size, method_alignment)
創建出一個ArtMethod。
而這個方法的作用就是分配空間,因爲i是連續的,所以分配出來的空間也是連續的。
這是隻是分配出內存空間,還沒有對ArtMethod的各個成員賦值,不過這並不影響觀察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"); // 計算出 f1()的地址
size_t secMid = (size_t) env->GetStaticMethodID(nativeStructsModelClazz, "f2", "()V"); // 計算出 f2()的地址
size_t methSize = secMid - firMid; //因爲f2和f1是相鄰的,所以 兩者相減就是一個 ArtMethod的大小
memcpy(smeth, dmeth, methSize); //將size代入,完成替換
值得一提的是,由於忽略了底層ArtMethod的結構差異,對於所有Android版本都不需要區分,而統一以 memcpy()
實現即可,代碼量大大減少。
即使以後的Android版本不斷修改ArtMethod的成員,只要保證ArtMethod數組仍是以線性結構排列,就無須再做適配。
2.2 訪問權限的問題
(1)方法調用時的權限檢查
看到這裏,你可能會產生疑惑:我們只是替換了ArtMethod的內容,但替換方案的所屬類,和原有方法的所屬類,是不同的類型,被替換的方法有權限訪問這個類的其他private方法嗎?
以這段簡單的代碼爲例子:
public class Demo {
Demo() {
func();
}
private void func() {
}
}
假如我們想要替換func()
方法,會不會因爲它是private的關係,而在構造函數中訪問不了。
來看看 Demo()
這個構造函數的Dex Code和Native Code,看看它是怎麼調用 func()
d的
void com.rikkatheworld.demo.Demo.<init>() (dex_method_idx=20628)
DEX CODE:
... ...
0x0003: 7010 9550 0000 | invoke-direct {v0}, void com.rikkatheworld.demo.Demo.func() //method@20629
... ...
CODE: (code_offset=0x006fd86c size_offset=0x006fd868 size=150)...
... ...
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_method_的偏移
0x006fd8d4: f8706800 ldr x0, [x0, x16] ;得到Demo.fun()的ArtMethod*
0x006fd8d8: f940181e ldr lr, [x0, #48] ;取得其entry_point_from_quick_compiled_code_
0x006fd8dc: d63f03c0 blr lr ;跳轉執行
這個調用邏輯和之前Activity的例子大同小異,需要注意的地方是,在構造函數調用同一個類的私有方法func()
時,沒有做任何權限檢查。
也就是說,這時即使把 func()
偷樑換柱,也能直接跳過去正常執行而不會報錯。
可以推測,在 dex2oat生成AOT機器碼時是做一些檢查和優化的,由於在dex2oat編譯機器碼時確認了兩個方法同屬一個類,所以機器碼中就不存在權限檢查的相關代碼。
(2)同名包下的權限問題
但是,並非所有的方法都可以這麼順利的訪問,我們發現補丁中的類在訪問同名包下的類時,會報訪問權限異常:
Caused by: java.lang.IllegalAccessError:
Method 'void com.rikkatheworld.demo.BaseBug.test()' is inaccessible to class 'com.rikkatheworld..demo.MyClass' (declaration of 'com.rikkatheworld.demo.MyClass' ...)
雖然 com.rikkatheworld.demo.BaseBug
和 com.rikkatheworld.demo.MyClass
是同一個包 com.rikkatheworld.demo
下面的,但是由於我們替換了 com.rikkatheworld.demo.BaseBug.test()
,而這個替換了的 BaseBug.test()
是從補丁包的Classloader中加載的,與原有的base包就不是同一個Classloader了,這樣就導致兩個類無法被判別爲同包名。
具體的校驗邏輯在虛擬機代碼的 Class::IsInSamePackage
中:
// 8.0 art\runtime\mirror\ class.cc
bool Class::IsInSamePackage(ObjPtr<Class> that) {
ObjPtr<Class> klass1 = this;
ObjPtr<Class> klass2 = that;
if (klass1 == klass2) {
return true;
}
if (klass1->GetClassLoader() != klass2->GetClassLoader()) { // 1
return false;
}
while (klass1->IsArrayClass()) {
klass1 = klass1->GetComponentType();
}
while (klass2->IsArrayClass()) {
klass2 = klass2->GetComponentType();
}
if (klass1 == klass2) {
return true;
}
std::string temp1, temp2;
return IsInSamePackage(klass1->GetDescriptor(&temp1), klass2->GetDescriptor(&temp2));
}
註釋1:判斷兩個類的類加載器是不是爲同一個,如果不是則返回false。
而不同類的ClassLoader當然是不同的。
知道了原因就可以解決問題,我麼你只要設置新類的Classloader爲原來類就可以了。
而這一步同樣不需要在JNI層構造底層的結構,只需要通過反射進行設置,這樣仍舊能夠保證良好的兼容性,實現代碼如下:
Field classLoaderField = Class.class.getDeclaredField("classLoader");
classLoaderField.setAccessible(true);
classLoaderField.set(newClass, oldClass.getClassLoader());
這樣通過反射替換了新類的類加載器,就可以解決同包名下的訪問權限問題。
(3)反射調用非靜態方法產生的問題
當一個非靜態方法被熱替換後,在反射調用這個方法時,會拋出異常。
比如下面這個例子:
// 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.rikkatheworld.demo.BaseBug,
but got com.rikkatheworld.demo.BaseBug
上面的 expeteted和got的兩個類雖然是同名的,但是本質卻是不同的類。
前者是被熱替換的方法所屬的類,由於我們把它的ArtMethod的 declaring_class_
替換了,因此就是新的補丁類。
而後者作爲被調用的實例對象bb的所屬類,是原有的BaseBug。兩者是不同的。
在反射invoke這個方法時,在底層會調用到 InvokeMethod:
jobject InvokeMethod(const ScopedObjectAccessAlreadyRunnable& soa, jobject javaMethod,
jobject javaReceive, 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)) {
ThrowNullPoinerException("null receiver");
return false;
} else if (UNLIKELY(!o->InstanceOf(c))) {
return false;
}
return true;
}
o表示Method.invoke()
傳入的第一個參數,也就是作用的對象。也就是一開始bb對象。
c表示方法的ArtMethod所屬的類型。 也就是我們替換的test()
的 declaring_class。
因此,只有o是c的一個實例才能通過驗證,才能繼續執行後面的反射調用流程。
顯然,上面的例子中,c被改變過,和o不會匹配,導致出現問題。
由此可知,這種熱替換方式所替換的非靜態方法,在進行反射調用時,由於 VerifyObjectIsClass()
時舊類和新類不匹配,就會導致校驗不通過,從而拋出上面那個異常。
那爲什麼只有方法是非靜態纔有這個問題呢?因爲如果是靜態方法,是在類的級別直接進行調用的,就不需要接收對象實例作爲參數,所以就沒有這方面的檢查了。
對於這種反射調用非靜態方法的問題,我們會採用另一種冷啓動機制應對,後文再做分析。
2.3 即時生效帶來的限制
除了反射的問題,像本方案以及Andfix這樣直接在運行期修改底層結構的熱修復方案都存在着一個限制,那就是只能支持方法的替換。而對於補丁類裏面存在方法的增加或減少,以及成員字段的增加或減少的情況,都是不適用的。
原因是這樣的,一旦補丁類中出現了方法的增加或減少,就會導致這個類以及整個Dex的方法數的變化,方法數的變化伴隨着方法索引的變化,這樣在訪問方法時就無法正常的索引到正確的方法了。
而如果字段發生了增加或減少,和方法變化的情況一樣,所有字段的索引都會發生變化,並且更嚴重的問題是,如果在程序運行中某個類突然增加了一個字段,那麼對於原有的這個類的實例,他們還是原來的結構,這是無法改變的。而新方法使用到的這些舊的實例對象時,訪問新增字段就會產生不可預期的結果。
不過新增一個完整的、原有包裏面不存在的新類是可以的,這個不受限制。
總之,只有以下兩種情況是不適用的:
- 引起原有類中發生結構變化的修改
- 修復了的非靜態方法會被反射調用
而對於其他情況,這種方式的熱修復都可以任意使用。
雖然有一些使用限制,但是一旦滿足使用條件,這種熱修復方式是十分出衆的,它補丁小,加載迅速,能夠實時生效無需重啓App,並且具有完美的設備兼容性。針對較小程度的修改可以採用本文這種即時生效的熱修復方案,並且可以結合資源修復,做到資源和代碼的即時生效。
而如果觸及了上面提到的熱替換使用限制,對於比較大的代碼改動以及被修復方法被反射調用的情況,Sophix也提供了另一種完整的dex修復機制,不過需要App重新冷啓動買來發揮其更加完善的修復以及更新功能。從而做到無感知的應用更新。