熱修復原理學習(2)底層替換原理和突破底層差異的方法

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機器碼後,是這樣的:

 7void 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.BaseBugcom.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的方法數的變化,方法數的變化伴隨着方法索引的變化,這樣在訪問方法時就無法正常的索引到正確的方法了。

而如果字段發生了增加或減少,和方法變化的情況一樣,所有字段的索引都會發生變化,並且更嚴重的問題是,如果在程序運行中某個類突然增加了一個字段,那麼對於原有的這個類的實例,他們還是原來的結構,這是無法改變的。而新方法使用到的這些舊的實例對象時,訪問新增字段就會產生不可預期的結果。

不過新增一個完整的、原有包裏面不存在的新類是可以的,這個不受限制。

總之,只有以下兩種情況是不適用的:

  1. 引起原有類中發生結構變化的修改
  2. 修復了的非靜態方法會被反射調用

而對於其他情況,這種方式的熱修復都可以任意使用。

雖然有一些使用限制,但是一旦滿足使用條件,這種熱修復方式是十分出衆的,它補丁小,加載迅速,能夠實時生效無需重啓App,並且具有完美的設備兼容性。針對較小程度的修改可以採用本文這種即時生效的熱修復方案,並且可以結合資源修復,做到資源和代碼的即時生效。

而如果觸及了上面提到的熱替換使用限制,對於比較大的代碼改動以及被修復方法被反射調用的情況,Sophix也提供了另一種完整的dex修復機制,不過需要App重新冷啓動買來發揮其更加完善的修復以及更新功能。從而做到無感知的應用更新。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章