熱修復原理1:java代碼

無需通過升級APK來實現BUG修復,有人選擇插件化來解決,但是對於已經開發好的APP,移植成本非常高,既要學習插件化工具,又要對老代碼進行改造。

熱修復更加輕量、靈活,直接把補丁上傳到雲端,下拉補丁後立即生效。熱修復主要有兩種方案,底層替換和類加載,一般配合使用。

  • 底層替換方案:限制頗多,但時效性最好,加載輕快,立即見效。
  • 類加載方案:時效性差,需要重新冷啓動才能見效,但修復範圍廣,限制少。

1.底層替換原理

直接替換ART虛擬機中的ArtMethod結構可以達到即時生效。
直接替換的難點在於獲取ArtMethod結構的大小,由於ArtMethod可能被廠商修改,不能直接使用AOSP原始的ArtMethod,因此要想辦法兼容。

memcpy(oldmeth, newmeth, sizeof(ArtMethod));

通過ART虛擬機源碼,發現類的method空間是線性的,一個接一個緊密new出來的排列在數組中的。

android9.0/art/runtime/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);
  for (size_t i = 0; i < length; ++i) {
    new(reinterpret_cast<void*>(&ret->At(i, method_size, method_alignment))) ArtMethod;
  }
  return ret;
}

根據這個特性可以看出,兩個相鄰ArtMethod的差值就是ArtMethod的大小,我們可以自己構造一個類來巧妙獲取。

public class NativeMethodModel {
  public static void f1(){}
  public static void f2(){}
}

可以在JNI層獲取它們的地址差值:

size_t firMid = (size_t) env->GetStaticMethodID(nativeMethodModelClazz, "f1", "()V");
size_t secMid = (size_t) env->GetStaticMethodID(nativeMethodModelClazz, "f2", "()V");

size_t methSize = secMid - firMid;

這個methSize就可以作爲sizeof(ArtMethod)的值了。

memcpy(oldmeth, newmeth, methSize);

訪問權限問題:

  • 方法調用時的權限檢查
    新替換的方法的所屬類,和原先方法的所屬類,是不同的類,被替換的方法有權限訪問這個類的其他private方法嗎?
    通過oat code觀察,調用同一個類的私有方法,沒有任何權限檢查,可以推測是編譯時的優化,確認了兩個方法同屬一個類,所以機器碼不做任何權限檢查。

  • 同包名下的權限問題
    補丁中的類在訪問同包名下的類時,會報異常,是由於補丁類是從補丁包的Classloader加載的,與原來的base包不是同一個Classloader。可以使用反射修改ClassLoader規避:

Field classLoaderField = Class.class.getDeclaredField("classLoader");
classLoaderField.setAccessible(true);
classLoaderField.set(newClass, oldClass.getClassLoader());
  • 反射調用非靜態方法產生的問題
    當一個非靜態方法被熱替換後,在反射調用這個方法時,會拋異常:
Caused: java.lang.IllegalArgumentException:
  Excepted receiver of type com.patch.demo.BaseBug
, but got com.patch.demo.BaseBug

com.patch.demo.BaseBug是兩個不同的類,前者是被熱替換方法所屬的類,由於我們替換了ArtMethod的declaring_class_,因此就是新的補丁類。後者是被調用的實例對象所屬類,是原有的BaseBug。
靜態方法是類級別直接調用的,不需要接收對象實例作爲參數。
這種反射調用非靜態方法產生的問題可以通過冷啓動對付。

新增方法、字段的影響:
除了反射問題,補丁類裏面存在方法、字段的新增或者減少,都是不適用的。
方法、字段數量的變化,會導致dex中的方法索引、字段索引發生變化,所以無法正常替換。

2.你所不知的Java

3.冷啓動類加載原理

4.多態對冷啓動類加載的影響

5.Dalvik下完整DEX方案的新探索

參考

阿里Sophix《深入探索Android熱修復技術原理》

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