Android熱修復學習筆記(三):冷啓動修復和其他資源修復

 冷啓動的作用在於突破了熱替換方案無法新增類方法的限制。可以更好地達到修復目的。

冷啓動的原理

 冷啓動的大致流程爲:提供dex差量包,整體替換dex。將補丁dex和應用的classes.dex合併爲一個完整的dex。完整的dex加載得到的dexFile對象作爲參數構建一個Element對象然後整體替換掉舊的dex Elements數組。

著名的CLASS_ISPREVERIFIFED錯誤

 首先,介紹一下在davilk虛擬機上特有的“CLASS_ISPREVERIFIFED”錯誤。
 在apk第一次安裝時,會對dex執行類校驗(用於校驗類的合法性,防止被篡改),成功後會進行類優化,把部分指令優化成虛擬機內部指令。如果apk只有一個dex,那麼這個dex就不會被打上CLASS_ISPREVERIFIFED標記。這時假如A類是補丁類,在單獨的補丁dex中,類B中的某個方法引用到了補丁類A,該方法在執行時會嘗試解析類A。如果此時類B被打上了CLASS_ISPREVERIFIFED標記,虛擬機會判斷類A和類B是不是同一個dex,如果不是那麼就會報錯。davilk虛擬機由於只有一個dex,所以這個問題是必須解決的。
 對於這個問題,業界提出許多解決方案。

(1)插樁

 通過.class字節碼修改技術在dex所有類的構造函數中都引用一個單獨的無關幫助類。這個幫助類會被放到單獨的dex中,這樣,因爲有兩個dex,CLASS_ISPREVERIFIFED就不會被設置。
 這個方案的缺點在於,會造成極大的性能損耗。CLASS_ISPREVERIFIFED的設置,使得類的校驗和優化僅僅在第一次安裝後被執行一次即可。在日後的加載使用中,只要判斷了CLASS_ISPREVERIFIFED標誌被設置,那麼就會跳過類的校驗和優化。去除CLASS_ISPREVERIFIFED,則會使類的校驗和優化在每次類加載中都會被執行。

(2)將補丁類加入到原有的pResClasses數組中

 在進行CLASS_ISPREVERIFIFED校驗之前,虛擬機會先從pResClasses數組中查詢是否存在想要的類。pResClasses數據保存有已經加載過的類。如果虛擬機預先能從pResClasses獲取想要的類,那麼就不會走CLASS_ISPREVERIFIFED校驗了。
 這個方案的缺點在於:會在jni層直接操作dex中的類和索引id,而dexopt後,odex層面的優化會寫死字段和方法的訪問偏移,導致我們類和索引id調用失敗。

(3)全量dex替換

 合成全新的dex來替換原有的dex文件,這也是適配Davilk虛擬機和Art虛擬機的優秀方案。目前騰訊的Tinker和阿里的sophix所用到方案也是基於此。在後面會着重介紹一下。

主流的冷啓動修復

 合成全新的dex來替換原有的dex文件的原理是什麼呢?Dalvik和Art都是通過dexfile.loadDex這個方法嘗試把一個dex文件解析加載到native中內存,通過dexFile.openDexFIleNative這個native方法對dex進行解析。如果查看這個方法可以發現。Dalvik在嘗試加載一個壓縮文件的時候,只會把classes.dex加載到內存中,所以如果有多個dex,除了主dex,其他dex都會被忽略。當然dalvik也只支持一個dex,在多dex時需要mutidex庫, 從主dex來調用分dex的方法。
 ART默認支持多dex,將補丁類放在主dex,即classes.dex中即可,後續分dex中相同的類是不會被加載的。我們在放置好之後,然後一起打包成一個壓縮軟件,通過dexFile.loadFIle得到dexFIle對象,將新的dexFIle替換舊的dexElements即可。

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

 在使用pResClasses預先設置這個方案時,會碰到一個難題,使用多態時往往會無法調用到正確的方法。這是爲什麼?
 首先我們要明白,多態的原理是什麼?
 實現多態的技術一般叫作動態綁定,是指在執行期間判斷所引用對象的實際類型,根據其實際的類型調用其相應的方法。多態一般指的是非靜態非私有方法的多態,field和靜態方法不具有多態性。
 我們來分析一下爲什麼會產生多態。假設A是父類,B是子類。首先new B()的執行會嘗試加載類B,方法調用鏈dvmResolveClass->dvmLinkClass->createVtable,此時會爲類B創建一個vtable,其實在虛擬機中加載每個類都會爲這個類生成一張vtable表,vtable表就是當前類的所有virtual方法的一個數組,當前類和所有繼承父類的public/protected/default方法就是virtual方法,因爲public/protected/default修飾的方法是可以被繼承的。private/static方法不屬於這個範疇,所以不能被繼承。
子類vtable的大小等於子類virtual方法數+父類vtable的大小。
 1.整體賦值父類vtable到子類的vtable
 2.遍歷子類的vtable方法合集,如果方法原型一致,說明是重寫父類父方法,那麼在想用的索引處,子類重寫方法覆蓋父類的方法。
 3.若方法不一致,那麼把該方法添加到vtable的末尾。方法在vtable的順序是根據方法被定義的順序來確定的。


 class Main {
    A a = new A();
    a.methodB();

}

 class A {
    //補丁類中新加的
    public void methodA() {
        System.out.println("this is method A");
    }

    // 原有的
    public void methodB() {
        System.out.println("this is method B");

    }
}


 如果我們在補丁中爲A新加了methodA,那麼實際上 a.methodB();最後輸出的是語句是“this is method A"。出現這種情況的原因是: a.methodB()在底層的實現方式可以理解爲vtable[0]=methodB,底層獲取了vtable[0],來進行實現。如果新增了方法methodA,那麼在底層的映射則是vtable[0]=methodA,vtable[1]=methodB,依舊獲取vtable[0]的話,會發現實現的對象變成了methodA。所以,在涉及到了兩個類時,只是單純地考慮其中一個類,有可能會導致方法調用錯亂的問題。
 那麼全量dex替換是如何解決這個問題的呢?大致思路是利用谷歌的開源的dexmerge方法,將補丁dex和原dex合併爲一個完整的dex,然後重新將dex載入內存,那麼此時每一個類都會重新載入,vtable也會被刷新,那麼這個問題就會被解決。其中需要注意的是如何更細顆粒化得生成補丁dex,避免在合成時造成內存風暴。

冷啓動修復需要注意的點

 我們在看一個問題。Application是程序的入口,熱修復的啓動再早也是不可能早於Application的載入的,所以Application類必定是在老dex中獲得,而不是在新的dex中獲取。Application初始化時,解析某個類,這時候補丁沒有加載,如果解析的類使用到了要修復的類(這時只能加載原始的類,而不能加載補丁中的類),那麼就會出現pre-verified問題。

解決方案:
 把Application裏面除了熱修復框架代碼之外的其他代碼都剝離開來,單獨提出放到一個其他類裏面,這樣使得Application不會直接用到過多非系統類,這樣保證單獨拿出來的類和Application處於同一個dex的概率極大。或者用反射方式訪問這個單獨的類。市面上很多熱修復框架都會要求將Application替換爲特定的Application,就是希望能夠接管實際的Application,將實際的Application和用戶代碼隔離開來,從而解決這個問題。

 除了修復代碼以外,還有一點需要的實現的是資源的修復,比如圖片資源,so文件修復等。

資源修復

 資源修復原理:

  1. 構造一個新的AssertManager,並通過反射調用addAssertPath,把這個完整的新資源包加入到assertManager中,這樣就得到了一個含有所有新資源的AssertManager
  2. 找到所有之前引用到了原有的AssertManager的地方,通過反射,把應用處替換爲AssertManager.

 so文件修復:
 soso文件的載入,都是調用了nativeLoad這個native方法加載so庫。所以關鍵在於如何實現so庫的重新載入。 so的註冊有動態和靜態兩種。動態註冊的native方法映射通過加載so庫過程中調用jni_onload方法調用完成,靜態註冊的native方法映射是在該native方法第一次執行的時候才完成映射,當然前提是該so庫已經加載過了。因此,對於動態註冊,我們只需要將so載入地址替換爲補丁類中的so地址即可。而對於靜態註冊,系統JNI爲我們提供瞭解註冊的接口,它會使得native方法無論是否實行後,都會重新去進行一次映射。

總結

 我很喜歡《深入探索Android熱修復技術原理》一書中所寫的,AndFix作者說的話,“AndFix作爲早期的熱修復方案,在如今優秀的熱修方法層出不窮的情況,勢必會被淘汰,希望它的思想能給未來的開發者一些啓發。”Android熱修復需要有對底層非常深刻的認識和見解,在書中,我看到了許多優秀,富有創新性的方案和令人傾佩的開發者的文章,這對於每一個開發者,都是寶貴的學習財富。

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