無需通過升級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熱修復技術原理》