前面我們提到的熱替換原理,根本是基于 native層方法的替換,是以當類的結構發生變化時,熱部署模式就會受到限制。
但是冷部署能突破這種限制,可以更好的達到修複目的,再加上冷部署在穩定性上有獨特的優勢,是以可以作為熱部署的有力補充而存在。
1. 冷啟動類加載原理
1.1 冷啟動實作方案概述
冷啟動重新開機生效,現在一般有兩種實作方案:
QQ空間 | Tinker | |
---|---|---|
原理 | 為了解決Dalvik下 unexpected dex problem異常而采用的插樁的方式,單獨放一個幫助類在獨立的dex中讓其他類調用,阻止了類被打上 辨別進而規避問題的出現。最後加載更新檔到dexFile對象作為參數建構一個Element對象插入到dexElments數組的最前面 | 提供dex差量包,整體替換dex的方案。差量的方式給出 ,然後将 patch.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
辨別。
-
類校驗,簡單來說,類校驗的目的是為了防止校驗類的合法性被篡改。此時會對類的每個方法進行校驗,這裡我們隻需要知道如果類的所有方法中直接引用到的類(第一層級關系,不會進行遞歸搜尋)和目前的類都在一個dex中的話,該方法就會傳回true。dvmVerifyClass()
-
類優化,簡單來說就這個過程會把部分指令優化成虛拟機的内部指令,比如方法調用指令dvmOptimizeClass()
變成了invoke-*
,quick指令會從類的vtable表中直接擷取,vtable簡單來說就是類的所有方法的一張大表,是以提高了方法的執行速率。invoke-*-quick
現在假定A類是更新檔類,是以更新檔A類在單獨的dex中。類B中的某個方法引用到更新檔類A,是以執行該方法會嘗試解析類A。
通過上面代碼得知,類B由于被打上了
CLASS_ISPREVERIFIED
标志,接下來referrer是類B,
resClassCheck
是更新檔類A,他們屬于不同的dex,就抛出了
dvmThrowIllegalAccessError
的異常了。
是以為了解決這個問題,而引申出插樁的方案,下面通過流程來介紹下這個方案:
- 建立一個單獨的無關幫助類,并将這個類打包時放到一個單獨的dex檔案中
-
原來的dex檔案(也就是我們自己原有的程式),所有的類的構造函數都要引入第一步中的類
當然不是我們自己手動在所有類的構造函數裡面加,而是通過
位元組碼修改技術,在所有的.class
類的構造函數中加入這個類的引用。.class
- 在安裝Apk的時期,會走到上述的
方法校驗類方法verifyAndOptimizeClass()
,在這個方法中由于發現了 原有的代碼(自己代碼)引用到了一個非本代碼所在dex的其他方法(即幫助類方法),是以dvmVerifyClass()
傳回了false,這就導緻虛拟機不會給所有的 類打上dvmVerifyClass()
辨別。CLASS_ISPREVERIFIED
- 基于步驟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語句内,該判斷檢驗兩個東西:
- 類的
是否為true(插樁方案就是使這個判斷為false)CLASS_ISPERVERIFIED
-
是否為falsefromUnverifiedConstant
而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檔案拿到以下檔案。
-
需要打包的類A的描述符,非必須,為了調試友善加上該參數而已preResolveClz
-
需要打包的類A所在的dex檔案的任何一個類描述符,注意,這裡不限定必須是引用更新檔類A的某個類,實際上隻要是同一個referClz
中的任意一個類都可以。是以我們直接拿原dex
中的第一個類即可。dex
-
需要打包的類A在原有dex檔案中的類索引ID。classIdx
然後通過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的情況下,
的第一個參數referrer類必須跟需要打包的類在同一個dex中,但是它們兩個類不需要存在任何引用關系,任何一個在同一個dex中的類作為referrer都可以。dvmResolveClass()
- 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
- 用
得到的DexFile完整替換掉 dexElements數組而不是插入。loadDex()
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方案,具體是如何實作的,請看下一章節。