天天看點

熱修複原理學習(4)冷啟動類加載原理1. 冷啟動類加載原理2 多态對冷啟動類加載的影響

前面我們提到的熱替換原理,根本是基于 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操作

熱修複原理學習(4)冷啟動類加載原理1. 冷啟動類加載原理2 多态對冷啟動類加載的影響

在第一次安裝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。

熱修複原理學習(4)冷啟動類加載原理1. 冷啟動類加載原理2 多态對冷啟動類加載的影響

通過上面代碼得知,類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方案提供了一種不同的思路:

熱修複原理學習(4)冷啟動類加載原理1. 冷啟動類加載原理2 多态對冷啟動類加載的影響

上圖是

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方案的不同點如下所示:

熱修複原理學習(4)冷啟動類加載原理1. 冷啟動類加載原理2 多态對冷啟動類加載的影響

需要注意:

  • 更新檔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方案,具體是如何實作的,請看下一章節。

繼續閱讀