熱修復原理學習(4)冷啓動類加載原理

前面我們提到的熱替換原理,根本是基於 native層方法的替換,所以當類的結構發生變化時,熱部署模式就會受到限制。
但是冷部署能突破這種約束,可以更好的達到修復目的,再加上冷部署在穩定性上有獨特的優勢,因此可以作爲熱部署的有力補充而存在。

1. 冷啓動類加載原理

1.1 冷啓動實現方案概述

冷啓動重啓生效,現在一般有兩種實現方案:

QQ空間 Tinker
原理 爲了解決Dalvik下 unexpected dex problem異常而採用的插樁的方式,單獨放一個幫助類在獨立的dex中讓其他類調用,阻止了類被打上CLASS_ISPREVERIFIED標識從而規避問題的出現。最後加載補丁到dexFile對象作爲參數構建一個Element對象插入到dexElments數組的最前面 提供dex差量包,整體替換dex的方案。差量的方式給出patch.dex,然後將 patch.dex與應用的classes.dex合併成一個完整的dex,完整dex加載得到的dexFile對象作爲參數構建一個Element對象然後整體替換掉舊的Elements數組
優點 沒有合成整包,產物比較小,比較靈活 自研dex差異算法,補丁包很小,dex merge成完整dex,Dalvik不影響類加載性能,Art下也不存在必須包含子類/引用類的情況
缺點 Dalvik下影響類加載性能,Art下類地址寫死,導致必須包含父類/引用類,最後補丁包很大 dex合併內存消耗在vm heap上,容易OOM,最後導致dex合併失敗

這裏對tinker方案的dex merge缺陷進行簡單說明。

dex merge操作是在Java層面進行的,所有對象的分配都是在java heap上完成的,如果此時進程申請的 java heap對象超過了 vm heap規定的大小,那麼進程發生OOM,系統memory killer可能會殺掉該進程,導致dex合成失敗。
另外一方面,我們知道JNI層面 C++ new/malloc申請的內存,分配中在 native heap,native heap的增長並不受vm heap大小的限制,只受限於RAM,如果RAM不足,那麼進程也會被殺死導致閃退。
所以如果只是從dex merge方面思考,在JNI層進行dex merge可以有效避免OOM,提高dex 合併的成功率,只是JNI層實現起來比較複雜而已。

Sophix另闢蹊徑,尋求一種既能無侵入打包,又能將熱部署模式作爲補充解決方案,下面分別的 DVM和ART虛擬機的冷啓動方案分別進行介紹。

1. 2 插樁實現的前因後果及造成的性能影響

如果僅僅是把補丁類打入補丁包中而不做任何處理的話,那麼在運行時類加載的時候會產生異常並且退出,接下來看下拋出這個異常的前因後果。

加載一個dex文件到本地內存的時候,如果不存在 odex文件,那麼首先會執行dexopt,dexopt的入口在 davilk/opt/OptMain.cpp的main方法中,最後調用到 verifyAndOptimizeClass()執行真正的 verify/optimize操作
在這裏插入圖片描述
在第一次安裝Apk的時候,會對原先dex執行dexopt,此時假如APK只存在一個dex,dvmVerifyClass()就會返回true。然後Apk中所有的類都會被打上 CLASS_ISPREVERIFIED的標識,接下來執行 dvmOptimizeClass(),類接着被打上 CLASS_ISOPTIMIZED標識。

  • dvmVerifyClass()
    類校驗,簡單來說,類校驗的目的是爲了防止校驗類的合法性被篡改。此時會對類的每個方法進行校驗,這裏我們只需要知道如果類的所有方法中直接引用到的類(第一層級關係,不會進行遞歸搜索)和當前的類都在一個dex中的話,該方法就會返回true。
  • dvmOptimizeClass()
    類優化,簡單來說就這個過程會把部分指令優化成虛擬機的內部指令,比如方法調用指令 invoke-*變成了 invoke-*-quick,quick指令會從類的vtable表中直接獲取,vtable簡單來說就是類的所有方法的一張大表,因此提高了方法的執行速率。

現在假定A類是補丁類,所以補丁A類在單獨的dex中。類B中的某個方法引用到補丁類A,所以執行該方法會嘗試解析類A。

在這裏插入圖片描述
通過上面代碼得知,類B由於被打上了 CLASS_ISPREVERIFIED標誌,接下來referrer是類B,resClassCheck是補丁類A,他們屬於不同的dex,就拋出了 dvmThrowIllegalAccessError的異常了。

所以爲了解決這個問題,而引申出插樁的方案,下面通過流程來介紹下這個方案:

  1. 創建一個單獨的無關幫助類,並將這個類打包時放到一個單獨的dex文件中
  2. 原來的dex文件(也就是我們自己原有的程序),所有的類的構造函數都要引入第一步中的類
    當然不是我們自己手動在所有類的構造函數裏面加,而是通過 .class字節碼修改技術,在所有的 .class類的構造函數中加入這個類的引用。
  3. 在安裝Apk的時期,會走到上述的verifyAndOptimizeClass()方法校驗類方法 dvmVerifyClass(),在這個方法中由於發現了 原有的代碼(自己代碼)引用到了一個非本代碼所在dex的其他方法(即幫助類方法),所以 dvmVerifyClass()返回了false,這就導致虛擬機不會給所有的 類打上 CLASS_ISPREVERIFIED標識。
  4. 基於步驟3,在冷啓動加載時,源程序引用了補丁類中的方法時,會去解析補丁類,走dvmResolveClass()方法,裏面會做檢驗,由於源程序所有類都沒有被打上 CLASS_ISPERVERIFIED標識,所以該方法不會拋出錯誤。

而上面步驟1所用到的無關幫助類,以及步驟2所涉及到的字節碼修改技術,就是插樁方案的核心。

但是插樁會給類加載效率帶來比較嚴重的影響,熟悉DVM開發的人知道,一個類的加載通常有三個階段: dvmResolveClass()dvmLinkClass()dvmlnitClass()dvmInitClass()階段在類解析完成並嘗試初始化類的執行,這個方法主要完成父類的初始化、當前類的初始化、靜態變量的初始化賦值等操作:

bool dvmInitClass(ClassObject* clazz) {
    if(clazz->status < CLASS_VERIFIED) {  // 1
        clazz->status = CLASS_VERIFYING;
        if (!dvmVerifyClass(clazz)) {  // 2
            ....
        }
        clazz->status = CLASS_VERIFIED;
    }
    if (!IS_CLASS_FLAG_SET(clazz, CLASS_ISPOTIMIZED) && gDvm.optimizing) {  //3
        dvmOptimizeClass(clazz, essentialOnly);  // 4
        SET_CLASS_FLAG(clazz, CLASS_ISOPTIMIZED);
    }
    ...
}

註釋1: 如果類沒有打上 IS_PERVERIFIED標識,那麼由於枚舉類型,它是小於 CLASS_VERIFIED的,即這個if裏面的內容會執行的。
註釋2:執行了 dvmVerifyClass()方法校驗類。

註釋3:如果類沒有打上 IS_OPTIMIZED的標識,那麼 註釋3的if語句裏面的內容會執行
註釋4:執行 dvmOptimizeClass做類優化。

綜上可知,如果類沒有在加載時的verifyAndOptimizeClass()的方法打上 CLASS_ISPERVERIFIED/CLASS_ISOPTIMIZED這兩個標識,則會在 類的初始化時去執行類的校驗和優化。
由於類檢驗的任務可以認爲是很重的,因爲會對類的所有方法中的所有指令都做校驗,單個類加載時耗時並不多,但是同一個時間點加載大量的類情況下,這種耗時就會被放大。所以這也是插樁給類的加載效率帶來比較大影響的後果。(因爲這些情況都是放在第一次安裝Apk的時候做的)

我們知道若採用插樁,會導致所有類都非perverify,從而導致類校驗和類優化會在類加載時觸發。
平均每個類檢驗與優化的耗時並不長, 而且這個耗時每個類只有一次(類只會加載一次)。但是由於在應用剛啓動這種場景下一般會同時加載大量的類,因此在這種情況下影響還是蠻大的,啓動的時候就容易白屏,這一點用戶是無法容忍的。

1.3 避免插樁的QFix方案

手機QQ熱補丁輕量級 QFix方案提供了一種不同的思路:
在這裏插入圖片描述

上圖是 dvmResolveClass()的內容,從1.2節中我們知道,如果不插樁,會走到上圖的第二個方框的if語句內,該判斷檢驗兩個東西:

  • 類的CLASS_ISPERVERIFIED是否爲true(插樁方案就是使這個判斷爲false)
  • fromUnverifiedConstant是否爲false

而QFix的思路則是將着手點放在fromUnverifiedConstant上,如果它爲true,那麼就不用再使用 插樁的方案了。
那該怎樣改變這個字段的值呢?

我們首先要保證 resClass不爲null,即 dvmDexGetResolvedClass()的返回結果不爲null,如果保證這個方法的返回值不爲null呢?
我們只需要調用過一次 dvmDexSetResolvedClass(pDvmDex, classIdx, resClass);就可以了,下面舉個例子來簡單說明:

public class B {
    public static void test() {
        A.a();
    }
}

我們此時需要打包的類A,所以類A被打入到一個獨立的補丁 dex文件中。
那麼執行到類B的test方法時,A.a(); 這行代碼就會嘗試去解析類A,此時進行 dvmResolveClass(const ClassObject* referrer, u4 classIdx, bool fromUnverifiedConstant).

  • referrer:實際上是類B
  • classIdx:類A在原dex文件結構類區中的索引ID
  • fromUnverifiedConstant:是否執行 const-class/instance-of 指令

此時調用的是類A的靜態a方法, invoke-static指令不屬於 const-class/instance-of這兩個指令。如果不做任何處理,dvmDexGetResolvedClass()的返回值時null。因爲類A是從補丁dex中解析加載的,類B是在原dex中,所以 B->pDvmDex != A->pDvmDex,接下來就會看到 dvmThrowIllegalAccessError從而導致運行異常。

爲了避免異常,必須要在開始的時候,就把補丁類A添加到原有dex(pDvmDex)的pResClasses數組中。這樣就確保了在執行類B的test方法時,dvmDexGetResolvedClass()返回值不爲null了。這樣就會在圖片中註釋3的下面一行,直接返回這個resClass,而不會去執行後續類A和類B的dex一致性校驗了。

具體實現,首先通過補丁工具反編譯dex爲smali文件拿到以下文件。

  • preResolveClz
    需要打包的類A的描述符,非必須,爲了調試方便加上該參數而已
  • referClz
    需要打包的類A所在的dex文件的任何一個類描述符,注意,這裏不限定必須是引用補丁類A的某個類,實際上只要是同一個dex中的任意一個類都可以。所以我們直接拿原dex中的第一個類即可。
  • classIdx
    需要打包的類A在原有dex文件中的類索引ID。

然後通過dlopen 拿到 libdvm.so庫的句柄,通過 dlsym拿到該so庫的 dvmResolveClass/dvmFindLoadedClass函數指針。首先需要預加載引用類xxx/xxx/class,這樣dvmFindLoadedClass(xxx/xxx/class)返回值纔不爲null,然後 dvmFindLoadedClass()的執行結果得到的 ClassObejct作爲第一個參數執行 dvmResolvedClass(class,id ,true)即可。

下面來看下JNI層代碼實現。實際上可以看到 preResolveClz參數是非必須的:

jboolean resolveClodPatchClasses(JNIEnv *env, jclass clz, jstring preResolveClz, jstring refererClz, jlong classIdx, dexstuff_t *dexstuff) {
    LOGD("start resolveClodPatchClasses");
    ClassObject *refererObj = dexstuff->dvmFindLoadedClass_fnPtr(
            Jstring2CStr(env, refererClz));   //通過refererClz 調用dvmFindLoadedClass加載補丁類
    LOGD("referrer ClassObject: %s\n", refererObj->decriptor);
    if (strlen(refererObj->descriptor) == 0) {
        return JNI_FALSE;
    }
    ClassObject *resolveClass = dexstuff->dvmResolveClass-fnPtr(refererObj, classIdx, true);   //調用dvmResolveClass方法
    LOGD("classIDx ClassObject: %s\n", resolveClass->descriptor);
    if (strlen(resolveClass->descriptor) == 0) {
        return JNI_FLASE;
    } 
    return JNI_TRUE;
}

這個思路不同於去Hook某個系統方法,而是從native層直接調用,同時更不需要插樁。具體實現需要注意以下3點:

  • dvmResolveClass的第三個參數 fromUnverifiedConstant必須爲true。
  • 在Apk多dex的情況下,dvmResolveClass()的第一個參數referrer類必須跟需要打包的類在同一個dex中,但是它們兩個類不需要存在任何引用關係,任何一個在同一個dex中的類作爲referrer都可以。
  • referrer類必須提前加載。

然而,QFix的方案有它獨特的缺陷,由於是在dexopt後繞過的,dexopt會改變原有的很多邏輯,許多odex層面的優化會固定字段和方法的訪問偏移,這就會導致比較嚴重的bug,在2.2節會詳細講解這一影響。最後採用的是自研的全量dex方案,具體在下一章講解。

1.4 Art下冷啓動實現

前面說過補丁在熱部署模式下是一個完整的類,補丁的粒度是類。現在的需求是補丁既能走熱部署模式也能走冷啓動模式,爲了減小補丁包的大小,並沒有爲熱部署和冷啓動分別準備一套補丁,而是在同一個熱部署模式下補丁能夠降級直接走冷啓動,所以不需要做dex merge。

但是通過前面的閱讀,我們知道了爲了解決Art下類地址寫死的問題,Tinker通過 dex merge成一個全新完整的新dex整體替換掉舊的dexElements數組。事實上,Art虛擬機下面默許已經支持多dex壓縮文件的加載。

下面分別來看一下 DVM和ART對 DexFile.loadDex()嘗試把一個dex文件解析加載到native中,內存都發生了什麼。實際上都是調用了 DexFile.openDexFileNative()這個native方法。看下 native層對應的 C/C++代碼具體實現。

(1)在DVM中的實現:

// dalvik/vm/native/dalvik_system_DexFile.cpp
static void Dalvik_dalvik_system_DexFile_openDexFileNative(const u4* args,
    JValue* pResult)
{
    ....
    if (hasDexExtension(sourceName)
            && dvmRawDexFileOpen(sourceName, outputName, &pRawDexFile, false) == 0) {  //加載一個原始dex文件
        ALOGV("Opening DEX file '%s' (DEX)", sourceName);

        pDexOrJar = (DexOrJar*) malloc(sizeof(DexOrJar));
        pDexOrJar->isDex = true;
        pDexOrJar->pRawDexFile = pRawDexFile;
        pDexOrJar->pDexMemory = NULL;
    } else if (dvmJarFileOpen(sourceName, outputName, &pJarFile, false) == 0) {  //加載一個壓縮文件
        ALOGV("Opening DEX file '%s' (Jar)", sourceName);

        pDexOrJar = (DexOrJar*) malloc(sizeof(DexOrJar));
        pDexOrJar->isDex = false;
        pDexOrJar->pJarFile = pJarFile;
        pDexOrJar->pDexMemory = NULL;
    } else {
        ALOGV("Unable to open DEX file '%s'", sourceName);
        dvmThrowIOException("unable to open DEX file");
    }
    ...
}

int dvmJarFileOpen(...){
    ...
    entry = dexZipFindEntry(&archive, kDexInJarName); /* kDexInJarName=="classes.dex",說明只加載一個dex */
    ...
}

dvmJarFileOpen()方法中,Dalvik嘗試加載一個壓縮文件的時候只會去把 classes.dex加載到內存中。如果此時壓縮文件中有多個dex文件,那麼除了classes.dex之外的其他dex文件將會被直接忽略掉。

在Art虛擬機下:方法調用鏈 DexFile_openDexFileNative -> OpenDex.FilesFromOat -> LoadDexFiles
具體代碼就不展示了,我們只需要知道,在Art下默認已經支持加載壓縮文件中包含多個dex,首先肯定加載primary dex也就是 classes.dex,後續會加載其他的dex。所以補丁類只需要放到classes.dex中即可,後續出現在其他dex中的“補丁類”是不會被重複加載的。
所以Sophix得到在Art最終的冷啓動方案:我們只要把補丁dex命名爲classes.dex。原Apk中的dex依次命名爲 classes(2,3,4...).dex就可以了,然後一起打包爲一個壓縮文件,在通過 DexFile.loadDex()得到DexFile對象,最後用該DexFile對象整體替換舊的dexElements數組就可以了。

Sophix方案和Tinker方案的不同點如下所示:
在這裏插入圖片描述

需要注意:

  • 補丁dex必須命名爲classes.dex
  • loadDex()得到的DexFile完整替換掉 dexElements數組而不是插入。

1.5 不得不說的其他點

我們知道DexFile.loadDex()嘗試把一個dex文件解析並加載到native內存, 在加載到native內存之前, 如果dex不存在對應的odex, 那麼Dalvik下會執行dexopt, Art下會執行dexoat, 最後得到的都是一個優化後的odex。 實際上最後虛擬機執行的是這個odex而不是dex。

現在有這麼一個問題,如果dex足夠大那麼dexopt/dexoat實際上是很耗時的,根據上面我們提到的方案, Dalvik下實際上影響比較小, 因爲loadDex僅僅是補丁包。 但是Art下影響是非常大的, 因爲loadDex是補丁dex和apk中原dex合併成的一個完整補丁壓縮包, 所以dexoat非常耗時。
所以如果優化後的odex文件沒生成或者沒生成一個完整的odex文件, 那麼loadDex便不能在應用啓動的時候進行的, 因爲會阻塞loadDex線程, 一般是主線程。 所以爲了解決這個問題, 我們把loadDex當做一個事務來看, 如果中途被打斷, 那麼就刪除odex文件, 重啓的時候如果發現存在odex文件, loadDex完之後, 反射注入/替換dexElements數組, 實現patch。 如果不存在odex文件, 那麼重啓另一個子線程loadDex, 重啓之後再生效。

另外一方面爲了patch補丁的安全性, 雖然對補丁包進行簽名校驗, 這個時候能夠防止整個補丁包被篡改, 但是實際上因爲虛擬機執行的是odex而不是dex, 還需要對odex文件進行md5完整性校驗, 如果匹配, 則直接加載。 不匹配,則重新生成一遍odex文件, 防止odex文件被篡改。

1.6 完整的方案考慮

代碼修復冷啓動方案由於它的高兼容性, 幾乎可以修復任何代碼修復的場景, 但是注入前被加載的類(比如:Application類)肯定是不能被修復的。 所以我們把它作爲一個兜底的方案, 在沒法走熱部署或者熱部署失敗的情況, 最後都會走代碼冷啓動重啓生效, 所以我們的補丁是同一套的。 具體實施方案對Dalvik下和Art下分別做了處理:

  • Dalvik下通過巧妙的方式避免插樁, 沒有帶來任何類加載效率的影響。
  • Art下本質上虛擬機已經支持多dex的加載, 我們要做的僅僅是把補丁dex作爲主dex(classes.dex)加載而已。

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

前面我們知道冷啓動方案几乎可以修復任何場景的代碼缺陷,但Dalvik下的QFix方案存在很大的限制,下面將深入介紹在目前方案下爲什麼會有這些限制,同時給出具體的解決方案。

2.1 重新認識多態

實現多態的技術一般叫做動態綁定,是指在執行期間判斷所引用對象的實際類型,根據其實際的類型調用其相應的方法。多態一般指的是非靜態私有方法的多態,field和靜態方法不具有多態性。示例如下:

public class B extends A {
    Strign name = "B name";
 
    @Override
    void a_t1() {
        System.out.println("B a_t1");
    }
    void b_t1(){}

    public static void main(String[] args) {
       A obj = new B();
       System.out.println(obj.name);
       obj.a_t1();
    }
}

class A {
    String name = "A name";
    
    void a_t1() {
        System.out.println("A a_1...");
    } 
    void a_t2();
}

輸出結果: A name / B a_t1

可以看到name這個field沒有多態性,print這個方法具有多態性,這裏先分析一下方法多態性的實現。首先 new B()的執行會嘗試加載類B,方法調用鏈 dvmResolveClass->dvmLinkClass->createVtable,此時會爲類B創建一個vtable,其實在虛擬機中加載每個類都會爲這個類生成一張vtable表,vtable表就是當前類的所有virtual方法的一個數組,當前類和所有繼承父類的public/protected/default方法就是virtual方法,因爲public/protected/default修飾的方法是可以被繼承的。private/static方法不屬於這個範疇,因爲不能被繼承。

這裏就不放 createVtable()的代碼了,有興趣的可以自行上網查閱,這裏來大概說一下它做了什麼,子類vtable的大小等於子類virtual方法數+父類vtable的大小:

  • 整體複製父類的vtable到子類的vtable
  • 遍歷子類的virtual方法集合,如果方法原型一致,說明是重寫父類方法,那麼在相同索引位置處,子類重寫方法覆蓋掉vtable中父類的方法
  • 若方法原型不一致,那麼把該方法添加到vtable末尾。

所以在上述示例中,假如父類A的vtable是 vtable[0]=A.a_t1, vtable[1]=A.a_t2,那麼B類的vtable就是 vtable[0]=B.a_t1, vtable[1]=A.a_t2, vtable[2]=B.b_t1。接下來 obj.a_t1()發生了什麼。invoke-virtual指令的解釋如下:

...
   if(methodCallRange) {
       thisPtr = (Object*) GET_REGISTER(vdst);
   } else {
       thisPtr = (Object*) GET_REGISTER(vdst & 0x0f); //當前對象
   }
   
   baseMethod = dvmDexGetReslvedMethod(methodClassDex, ref);  //是否已經解析過該方法
   if(baseMethod == NULL) {
       baseMethod = dvmResolveMethod(curMethod->clazz, ref, METHOD_VIRTUAL);
       //沒有解析過該方法調用 dvmResolveMethod,baseMethod得到的當然是A.a_t1方法
       ...
   }

   methodToCall = thisPtr->clazz->vtable[baseMethod->methodIndex]; /* A.a_t1方法在類A的vtable中的索引去類B的vtable中查找
   ...

首先 obj 引用類型是基類A,所以上述代碼中 baseMethod拿到的是 A.a_t1()baseMethod->methodIndex是該方法在類A的vtable中的索引0,obj的實際類型是類B,所以thisPtr->clazz就是類B,那麼 B.vtable[0]就是 B.a_t1()方法,所以 obj.a_t1()實際上最後調用的是 B.a_t1()方法。這樣就實現了方法的多態。

至於field/static方法爲什麼不具有多態性,這裏不進行詳細的代碼分析,有需要的可以看 iget/invoke-static的指令解釋,簡單來講,是從當前變量的引用類型而不是實際類型中查找,如果找不到,再去父類中遞歸查找。
所以field和static方法不具備多態性。

2.2 冷啓動方案限制

下面來看一下如果新增了一個 public/protected/default方法,會出現什麼情況。

public class Demo {
    public static void test_addMethod(){
        A obj = new A();
        obj.a_t2();
    }
}

class A {
    int a =0;

    //新增a_t1方法
    void a_t1() {
        Log.d("Sophix","A a_t1");
    }

    void a_t2() {
        Log.d("Sophix","A a_t2");
    }
}

修復後的APK中新增了 a_t1()方法,DEMO類不做任何的修復,測試發現應用補丁後Demo.test_addMethod()得到的結果竟然是 Sophix: A a_t1,這表明 obj.a_t2()執行的是 a_t1()方法,下面深入分析一下本質原因。

在 2.1節提到過,在dex文件第一次加載的時候,會執行dexopt,dexopt有 verify和optimize兩個過程,那分別就是類校驗和類優化。

這裏主要介紹一下 optimize階段:

//Android4.4 dalvik/vm/analysis/Optimize.cpp
void dvmOptimizeClass(ClassObject* clazz, bool essentialOnly)
{
    int i;

    for (i = 0; i < clazz->directMethodCount; i++) {
        optimizeMethod(&clazz->directMethods[i], essentialOnly); // 1
    }
    for (i = 0; i < clazz->virtualMethodCount; i++) {
        optimizeMethod(&clazz->virtualMethods[i], essentialOnly); // 2
    }
}

static void optimizeMethod(Method* method, bool essentialOnly)
{
   ...
        /*
         * non-essential substitutions:
         *  invoke-{virtual,direct,static}[/range] --> execute-inline
         *  invoke-{virtual,super}[/range] --> invoke-*-quick
         */
        if (!matched && !essentialOnly) {
            switch (opc) {
            case OP_INVOKE_VIRTUAL:
                if (!rewriteExecuteInline(method, insns, METHOD_VIRTUAL)) {  
                    rewriteVirtualInvoke(method, insns,   //4
                        OP_INVOKE_VIRTUAL_QUICK);
                }
                break;
           ....
}

註釋1:對direct方法進行類優化(即不能繼承的方法)
註釋2:對virtual方法進行類優化(即可以繼承的方法)

註釋3:如果是虛方法,重寫 invoke-virtual爲虛擬機內部指令 invoke-virtual-quick,這個指令後面跟的立即數(insns)就是該方法在類vtable中的索引值

invoke-virtual-quick 效率比 invoke-virtual更高,因爲它直接從實際類型的vtable中獲取調用方法指針,而省略了 dvmResolveMethod從變量的引用類型獲取方法在vtable索引ID的步驟,所以更高效。

所以很容易知道在上面代碼中示例中,方法調用錯亂髮生的本質原因了。打包前類A的 vtable值時 vtable[0]=a_t2。打包後類新增了a_t1方法, 那麼類A的vtable值爲 vtable[0]=a_t1, vtable[1]=a_t2,但是 obj.a_t2()這行代碼在odex中的指令實際上是 invoke-virtual-quick A.vtable[0],所以導包前調用的是 a_t2()方法,打包後調用的是 a_t1方法,導致了方法的調用錯亂。
(其實就是加載期類優化所導致的)

2.3 終極解決方案

可見,由於多態的影響,QFix的方案最終會遭到問題,我們最後的希望就是寄託於類似Tinker方案的完整dex解決方案。

利用Google已經開源的DexMerge方案,把補丁dex和原dex合併成一個完整的dex似乎是可行的,但僅僅這樣還是不夠的,多dex下如果DexMerge拋出了65535方法數超了異常,DexMerge會導致內存風暴,在內存不足的情況下容易更新失敗。完整的dex合成要求在移動端進行,且實現較爲複雜。

因此,Sophix自研了一套完整的dex方案,具體是如何實現的,請看下一章節。

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