Android 熱修復(fù)技術(shù)

熱修復(fù)技術(shù)一般是對線上bug的緊急處理,不需要二次安裝應(yīng)用,在用戶無感知的情況下就可以修復(fù)已知的bug。而插件化技術(shù)是把需要實現(xiàn)的模塊和功能獨立提取出來,減少宿主的規(guī)模,需要相應(yīng)的功能時再去加載相應(yīng)模塊。熱修復(fù)要完成或滿足以下三個方面的需求,才能解決行業(yè)痛點。

  1. 代碼熱修復(fù)技術(shù)
  2. 資源熱修復(fù)技術(shù)
  3. so庫熱修復(fù)技術(shù)

1. 代碼修復(fù)技術(shù)

1.1 類加載機制

當(dāng)我們調(diào)用DexClassLoader調(diào)用loadDex()的時候,如果不存在odex則會執(zhí)行dexopt()方法,會先對DexFile每個文件的class文件進行校驗和優(yōu)化,代碼如下:

static void verifyAndOptimizeClass (DexFile* pDexFile, ClassObject* clazz, const DexClassDef*                pClassDex, bool doVerity, bool doOpt){ 
    const char* classDescriptor;
    bool verified = false;
    classDescriptor = dexStringByTypeIdx(pDexFile, pClassDef->classIdex);
    
    if(doVerify){
        if(dvmVertifyClass(clazz)){ //執(zhí)行類的Verify
            //類被打上 CLASS_ISPREVERIFIED標(biāo)志
            ((DexClassDef*)pClassDef)->accessFlags |= CLASS_ISPREVERIFIED;
            verified = true;
        }
    }
    
    if(doOpt){
        bool needVerify = (gDvm.dexOptMode == OPTIMIZE_MODE_VERIFIED || gDvm.dexOptMode ==                                     OPTIMIZE_MODE_FULL);
        if(!verified && needVerify){
            //......
        } else {
            dvmOptimizeClass(clazz, false); //執(zhí)行類的optimize
            //類被打上 CLASS_ISOPTIMIZED標(biāo)志
            ((DexClassDef*)pClassDef)->accessFlags |= CLASS_ISOPTIMIZED; 
        }
    }

}
  • dvmVerifyClass: 類校驗,類校驗的目的簡單來說是為了防止類被篡改校驗類的合法性。此時會對類的每個方法進行校驗,如果類的所有方法中直接引用到的類和當(dāng)前類都在同一個dex中的話,dvmVerifyClass就返回true。
  • dvmOptimizeClass:類優(yōu)化,簡單的來說這個過程會把部分指令優(yōu)化成虛擬機內(nèi)部指令,然后會把這些指令存入類的vtable表中,需要的話,直接取出來用,會提升執(zhí)行效率。

如果單純的將加載好的dex文件放入baseloader的dexelements數(shù)組的最前面,則會出現(xiàn)報錯,原因是:

ClassObject* dvmResolveClass(const ClassObject* referrer, u4 classIdex, bool fromUnverifiedConstant){
    ......
    //如果類被打上了CLASS_ISPREVERIFIED標(biāo)志
    if(!fromUnverifiedConstant && IS_CLASS_FLAG_SET(referrer, CLASS_ISPREVERIFIED)){
        if(referrer->pDvmDex != resClassCheck->pDvmDex && resClassCheck->classLoader != null){
            dvmThrowIllegalAccessError("Class ref in pre-verified class resolved to unexpected implementation"); //拋異常
            return NULL;
        }
    }
    ......
}

這就是類加載機制的問題所在,很多大廠都是為了解決這個問題而提出自家的方案。而dalvik虛擬機和art虛擬機的類加載機制又有所不同,dalvik只會從dex elements里面加載一個主dex,其余的在使用到了在加載。而art虛擬機本身就有對dex包有合成的功能,會一起把dex放到壓縮文件中,然后依次加載。下面來看看不同廠家對合成dex的解決方案。

1.2 合成方案

首先是QQ空間的方案,他們團隊在解決以上問題,用了一個獨立的dex包,讓其他不需要被標(biāo)記Vertify標(biāo)記都引入hack.class來規(guī)避以上問題,這樣導(dǎo)致的問題是會影響到dalvik虛擬機的加載性能,每次運行這個類的時候,才會校驗和優(yōu)化,art虛擬機在替換的時候,需要把它引用的類以及父類也放在patch.dex包里面,導(dǎo)致合成包很大。

騰訊的tinker方案,主要就是將他們的補丁dex合成到有問題的dex中,然后整體合成一個dex,替換原先的dex elements數(shù)組中去,這種方案就不會導(dǎo)致art環(huán)境下 合成包很大問題,但是在合成新的dex文件會消耗很多heap內(nèi)存,可能會出現(xiàn)oom的情況。

阿里的sophix方案,是移除dex文件中對內(nèi)部類的定義,而類的方法實體以及其他存在dex文件中的信息不移除,這么做的考慮是為了尋找方法的時候,指針發(fā)生偏移。這樣會就防止這個類被打上vertify的標(biāo)記,在art虛擬機環(huán)境下也有很好的表現(xiàn)。

Qfix方案,他們會在校驗dex包提前加載好所需的dex包,然后規(guī)避這個缺陷的產(chǎn)生,但是會導(dǎo)致dexopt方法執(zhí)行不是按正常的邏輯執(zhí)行的,會導(dǎo)致對class文件優(yōu)化的時候會寫死字段和方法的地址,在多態(tài)的情況下會導(dǎo)致調(diào)用的是另外一個方法的情況,這種情況就是方法的偏移量發(fā)生改變。

1.3 Application的處理

在Android應(yīng)用啟動的時候,Application是最先被加載的,在多dex情況下,主要有兩種方法解決問題:

  • 將Application用到所有的非系統(tǒng)類都和Application位于同一個dex包,這樣就可以保證pre-verfied標(biāo)志被打上,避免進入dvmOptResolveClass,在補丁加載完成之后,我們在清除pre-verified標(biāo)志,使得接下來使用其他類不會報錯。
  • Application可以采用反射的方式來訪問這個單獨類,這樣就可以把Application和其他類隔離開了。

第一種方案,我們可以在執(zhí)行dexopt的結(jié)束后把Application類的vertify標(biāo)志清除掉,然后加載完補丁dex之后,將Application的vertify標(biāo)志恢復(fù),在attachBaseContext時候,替換dex elements,這樣Application初始化的時候就不會報錯了。因為如果在Application初始化的時候,發(fā)現(xiàn)類vertify標(biāo)志沒有被打上,就會重新執(zhí)行該類所有引用到的class dvmOptResolveClass方法,等運行到補丁class的時候,又會報錯。第二種方案,也是Tinker解決問題的方式,一開始就dexopt TinkerApplication,運行之后然后再通過反射調(diào)用Application。

1.4 art環(huán)境下合成dex包存在的問題

如果dex包足夠大的話,art虛擬機loadDex的時候會將patch.dex和原dex包進行合成一個完整的dex包,這個過程非常的耗時,如果在應(yīng)用啟動的時候,odex文件沒有生成的話,會在主線程中去合成,所以不能在應(yīng)用啟動的時候合成。在應(yīng)用啟動的時候,看有沒有odex,如果有的話,通過反射注入,如果沒有則使用子線程去loadDex,重啟后再生效。合成失敗要將odex文件給刪除掉,同時通過md5對odex包進行校驗,看是否被篡改,如果不匹配,重新生成一遍odex。

2. 資源熱修復(fù)技術(shù)

2.1 Instant Run 方式實現(xiàn)資源修復(fù)

  • 通過構(gòu)造一個新的AssertManager,并通過反射調(diào)用addAssertPath,把這個完整的新資源包加入到AssertManager中,這樣就得到一個含有所有新資源的AssertManager。
  • 找到所有之前引用到原有AssertManager的地方,通過反射,把引用處替換為AssertManager。

這種方式必須要將整個AssertManager全部替換掉原來系統(tǒng)生成的AssertManager,因為不管在Android L之前版本還是之后版本,直接通過addAssertPath是無法生效的。

2.2 sophix方案

由于AssertManager實際處理資源的邏輯都在native層,Java層只是一個引用的地方,完全可以調(diào)用native層AssertManager的析構(gòu)函數(shù),然后重新初始化,將所需要添加到 resource path 添加進去,這樣native層就會處理這些資源文件,并不需要向Instant Run那樣做很多反射的工作。

sophix構(gòu)造了package id 為0x66開始的補丁包,這樣會導(dǎo)致之前的資源id會發(fā)生偏移,有以下三種情況:

  • 新增資源導(dǎo)致id偏移,將資源id改回原來的那個resource id。
  • 內(nèi)容發(fā)生改變的資源,需要改代碼將資源id改為新的那個資源id,0x66開頭的那個。
  • 刪除的資源,不要使用它即可。

3. so庫熱更新技術(shù)

3.1 so庫冷部署實現(xiàn)方案

冷部署有兩種方案可以實現(xiàn),一種是通過接口替換的方式,通過加載指定目錄下的so去實現(xiàn);另外一種是類似于類修復(fù)反射注入方式,只要把我們的補丁so庫的路徑插入到PathClassLoader.nativeLibraryDirectories數(shù)組的最前面就能夠達到加載補丁so庫的目的。

3.2 so庫熱部署實現(xiàn)方案

JNI的注冊方式有兩種,一種是動態(tài)注冊另外一種是靜態(tài)注冊,在動態(tài)注冊的情況下通過加載so的方式,art虛擬機可以完成實時修復(fù),但是dalvik虛擬機則還是返回之前so庫的句柄,這種只能通過修改so的名字來規(guī)避。

so庫的熱部署方案對靜態(tài)注冊的方案有一定的局限性,因為雖然可以通過調(diào)用以下接口:

static void patchNativeMethod(JNIEnv* env, jclass clz){
     env->UnregisterNatives(clz);
}

不管是靜態(tài)注冊還是動態(tài)注冊的native方法之前是否執(zhí)行過加載補丁so的時候都會重新去做映射。但是我們無法知道到底哪個native方法做了修改,而且就算做了修改,我們調(diào)用上面的方法,重新load補丁so庫也有可能修復(fù),也有可能不會被修復(fù)。因為,如果補丁so庫在gDvm.nativeLibs的位置在原so庫的下面,則不會被修復(fù)。而且還有個問題,就是受到dex的影響,如果so對應(yīng)的方法在dex包中沒有的話,會拋 NoSuchMethod的異常。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容