熱修復初探

拉丁吳發表於2016-08-30

熱修復這個技術點最近有點火,有QQ空間開發團隊為其背書,還有的大廠開源的熱修復框架,這些對於推動這項技術也起了很大的作用。作為一個有追求的工程師菜鳥,今天,我想通過幾篇文章把這種線上修復的解決思路以及幾種具體的實現方案理一遍。

在開始分析之前,首先需要說說熱修復解決了一個什麼問題,閉上眼睛,想象一個場景:當你的產品剛上線,發現有一個導致閃退的空指標異常的問題或者程式重大漏洞急需解決,那麼問題來了,怎麼修復?常規的,當然是修改完有問題的方法,然後趕緊再打包,推送到使用者強制更新,但是,現在還有另一種方式,就是程式線上下載修復檔案到本地,然後用修改過的類覆蓋原來有問題的類,這樣的使用者體驗是一顆賽艇的。

好了,回到熱修復這個話題。

首先我想先談談熱修復本身,熱修復是一種動態修復程式解決問題的思想,其本身是有很多不同的具體實現方案的,阿里的基於C/C++層操控method指標的Dexposed,AndFix,以及QQ空間的基於dex分包的HotFix,後者和前者的熱修復方案在原理上截然不同,可以說各有千秋。而我在查閱資料的時候,發現很多Blog都不夠嚴謹,往往標題聲稱熱修復技術但是隻解釋QQ空間的解決方案,可以說這種做法是容易誤導人的,雖然不能算錯誤,但是不太嚴謹。

目前的熱修復技術的解決方案有很多,我想就上面提到的兩種解決方案來做詳細的探討。

1,基於C層指標替換的Dexposed和AndFix

  • 這兩個熱修復的框架,在底層原理上是基本一致的,所以我想把他們放在一起探討,

他們都做了大致三件事:

  • 1,在C/C++層將Java層中出問題的方法修改為native方法
  • 2,獲取問題方法call到C層的指標
  • 3,通過獲取的指標做相應的操作:呼叫Java層的回撥方法繼續處理(DexPosed)或者直接通過反射呼叫Java層的補丁方法(AndFix)。

以Dexposed為例:

dexposed.jpg

至於具體的程式碼解釋,請直接看Android中免Root實現Hook的Dexposed框架實現原理解析以及如何實現應用的熱修復

這兩種熱修復框架的區別在於:

  • Dexposed暫時不支援ART模式,AndFix支援
  • AndFix方案更加成熟,更加自動化(畢竟是支付寶出的)

2,基於Dex分包的HotFix

這個解決方案很巧妙,基於Google推出的的Multidex方案,以ClassLoader的方式完成對問題類的替換。

所以這個問題一定會先談Android的分包方案:為了解決Android4.x系統中65536的方法數限制,Android推出Multidex方案,將一個完整的APK中的Dex拆分成好幾個dex,通過PathClassLoader 這個載入器來載入。

當點開程式的時候,PathClassLoader 會把分包的多個dex新增到父類中的一個DexPathList 中

DexPathList 詳情如下:

public class BaseDexClassLoader extends ClassLoader {

    private final DexPathList pathList;
}複製程式碼
/*package*/ final class DexPathList {
    private static final String DEX_SUFFIX = ".dex";
    private static final String JAR_SUFFIX = ".jar";
    private static final String ZIP_SUFFIX = ".zip";
    private static final String APK_SUFFIX = ".apk";

    /** class definition context */
    private final ClassLoader definingContext;

    /** list of dex/resource (class path) elements  也就是dex列表咯*/
    private final Element[] dexElements;

    /** list of native library directory elements */
    private final File[] nativeLibraryDirectories;複製程式碼

那麼當需要載入某個類的時候,是怎麼載入的呢?

//BaseDexClassLoader:  
    @Override  
    protected Class< ?> findClass(String name) throws ClassNotFoundException {  
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>(); 
        Class c = pathList.findClass(name, suppressedExceptions);  
        if (c == null) {  
            ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);  
            for (Throwable t : suppressedExceptions) {  
                cnfe.addSuppressed(t);  
           }  
        throw cnfe;  
        }  
        return c;  
    }複製程式碼

findClass()方法如下:

 public Class findClass(String name, List<Throwable> suppressed) {      
         for (Element element : dexElements) {  
           DexFile dex = element.dexFile;  
            if (dex != null) {  
                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);  
                if (clazz != null) {  
                    return clazz;  
                }  
            }  
       }  
        if (dexElementsSuppressedExceptions != null) {  
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));  
        }  
        return null;  
    }複製程式碼

如你所見,當需要載入一個類的時候,會在pathList中去尋找,並且是通過順序遍歷各個dex包的方式,一旦找到目標類,則停止遍歷

qqZone.png

這就給了我們一個想法,有沒有可能把打了補丁的dex插入到pathList中,當需要載入有問題的類的時候,根據遍歷,首先查到已經修復的類,遍歷結束,也就完成了修復。(當然了,這個想法是騰訊空間Android工程師想到的)

有了想法,也得有合適的載入器啊。結果你猜怎麼著?Android還真提供了這樣的機會。

在Android中也有三個類載入器,分別是UrlClassLoader,PathClassLoader,DexClassLoader.

  • UrlClassLoader 從Url列表中載入相關的jar檔案,但是dalvik無法直接識別jar,so.....
  • PathClassLoader 它只會去讀取 /data/dalvik-cache 目錄下的 dex 檔案,就是已安裝的apk,
  • DexClassLoader 可以用來從.jar和.apk型別的檔案內部載入classes、dex檔案。而且,它和PathClassLoader繼承自共同的父類。顯然,這是最合適的載入器。

android.png

好了,基本機制到這裡就結束了,還有一些問題卻沒有被提出來,不過網路上已經有了很好的解決方案了。

  • 如何防止自己的類被打上 CLASS_ISPREVERIFIED標誌
    • 這個標誌是虛擬機器的一種優化手段,打上這個標誌之後,就不會引用其他dex中的類,如果引用了,則報錯。解決方案也很簡單,就是在類中引用其他dex包的引用,具體方法請直接Google。

雖然現在我們公司的開發團隊肯定用不上熱修復技術,但是作為工程師卻必須對新技術有所研究。近期我會繼續研究熱修復 HotFix 框架的原始碼,有必要的話會對DexClassLoader如何動態的插入jar包或者dex檔案給出更詳細的解析,暫時沒有時間解釋了,大家先上車

臥槽 說錯話了.....

相關文章