熱修復初識

熱修復技術

APP提早發出去的包,如果出現客戶端的問題,實在是乾着急,覆水難收。因此線上修復方案迫在眉睫。

概述

基於Xposed中的思想,通過修改c層的Method實例描述,來實現更改與之對應的java方法的行爲,從而達到修復的目的。阿里的基於C/C++層操控method指針的Dexposed,AndFix,以及QQ空間的基於dex分包的HotFix,後者和前者的熱修復方案在原理上截然不同,可以說各有千秋。而我在查閱資料的時候,發現很多Blog都不夠嚴謹,往往標題聲稱熱修復技術但是隻解釋QQ空間的解決方案,可以說這種做法是容易誤導人的,雖然不能算錯誤,但是不太嚴謹。

1.Xposed

誕生於XDA論壇,類似一個應用平臺,不同的是其提供諸多系統級的應用。可實現許多神奇的功能。Xposed需要以越獄爲前提,像是iOS中的cydia。

Xposed可以修改任何程序的任何java方法(需root),github上提供了XposedInstaller,是一個android app。提供很多framework層,應用層級的程序。開發者可以爲其開發一些系統或應用方面的插件,自定義android系統,它甚至可以做動態權限管理(XposedMods)。

Android系統啓動與應用啓動

Zygote進程是Android手機系統啓動後,常駐的一個名爲‘受精卵’的進程。

  • zygote的啓動實現腳本在/init.rc文件中
  • 啓動過程中執行的二進制文件在/system/bin/app_process

任何應用程序啓動時,會從zygote進程fork出一個新的進程。並裝載一些必要的class,invoke一些初始化方法。這其中包括像:

  • ActivityThread
  • ServiceThread
  • ApplicationPackageManager

等應用啓動中必要的類,觸發必要的方法,比如:handleBindApplication,將此進程與對應的應用綁定的初始化方法;同時,會將zygote進程中的dalvik虛擬機實例複製一份,因此每個應用程序進程都有自己的dalvik虛擬機實例;會將已有Java運行時加載到進程中;會註冊一些android核心類的jni方法到虛擬機中,支撐從c到java的啓動過程。

Xposed做了手腳

Xposed在這個過程改寫了app_process(源碼在Xposed : a modified app_process binary),替換/system/bin/app_process這個二進制文件。然後做了兩個事:

  1. 通過Xposed的hook技術,在上述過程中,對上面提到的那些加載的類的方法hook。
  2. 加載XposedBridge.jar

這時hook必要的方法是爲了方便開發者爲它開發插件,加載XposedBridge.jar是爲動態hook提供了基礎。在這個時候加載它意味着,所有的程序在啓動時,都可以加載這個jar(因爲上面提到的fork過程)。結合hook技術,從而達到了控制所有程序的所有方法。

爲獲得/system/bin/目錄的讀寫權限,因而需要以root爲前提。

Xposed的hook思想

那麼Xposed是怎麼hook java方法的呢?要從XposedBridge看起,重點在 XposedBridge.hookmethod(原方法的Member對象,含有新方法的XC_MethodHook對象);,這裏會調到

private native synchronized static void hookMethodNative(Member method, Class<?> declaringClass, int slot, Object additionalInfo);

這個native的方法,通過這個方法,可以讓所hook的方法,轉向native層的一個c方法。如何做到?

When a transmit from java to native occurs, dvm sets up a native stack.
In dvmCallJNIMethod(), dvmPlatformInvoke is used to call the native method(signature in Method.insns).

在jni這個中間世界裏,類型數據由jni表來溝通java和c的世界;方法由c++指針結合DVM*系(如dvmSlotToMethod,dvmDecodeIndirectRef等方法)的api方法,操作虛擬機,從而實現java方法與c方法的世界。

那麼hook的過程是這樣:首先通過dexclassload來load所要hook的方法,分析類後,進c層,見代碼XposedBridge_hookMethodNative方法,拿到要hook的Method類,然後通過dvmslotTomethod方法獲取Method*指針,

Method* method = dvmSlotToMethod(declaredClass, slot);

declaredClass就是所hook方法所在的類,對應的jobject。slot是Method類中,描述此java對象在vm中的索引;那麼通過這個方法,我們就獲取了c層的Method指針,通過

SET_METHOD_FLAG(method, ACC_NATIVE);

將該方法標記爲一個native方法,然後通過

method->nativeFunc = &hookedMethodCallback;

定向c層方法到hookedMethodCallback,這樣當被hook的java方法執行時,就會調到c層的hookedMethodCallback方法。

通過meth->nativeFunc重定向MethodCallBridge到hookedMethodCallback這個方法上,控制這個c++指針是無視java的private的。

另外,在method結構體中有

method->insns = (const u2*) hookInfo;

用insns指向替換成爲的方法,以便hookedMethodCallback可以獲取真正期望執行的java方法。

現在所有被hook的方法,都指向了hookedMethodCallbackc方法中,然後在此方法中實現調用替換成爲的java方法。

從Xposed提煉精髓

回顧Xposed,以root爲必要條件,在app_process加載XposedBidge.jar,從而實現有hook所有應用的所有方法的能力;而後續動態hook應用內的方法,其實只是load了從zypote進程複製出來的運行時的這個XposedBidge.jar,然後hook而已。因此,若在一個應用範圍內的hook,root不是必須的,只是單純的加載hook的實現方法,即可修改本應用的方法。

業界內也不乏通過「修改BaseDexClassLoader中的pathList,來動態加載dex」方式實現熱修復。後者純java實現,但需要hack類的優化流程,將打CLASS_ISPREVERIFIED標籤的類,去除此標籤,以解決類與類引用不在一個dex中的異常問題。這會放棄dex optimize對啓動運行速度的優化。原則上,這對於方法數沒有大到需要multidex的應用,損失更明顯。而前者不觸犯原有的優化流程,只點殺需要hook的方法,更爲純粹、有效。

2.基於C層指針替換的Dexposed和AndFix

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

他們都做了大致三件事:
1,在C/C++層將Java層中出問題的方法修改爲native方法
2,獲取問題方法call到C層的指針
3,通過獲取的指針做相應的操作:調用Java層的回調方法繼續處理(DexPosed)或者直接通過反射調用Java層的補丁方法(AndFix)。

以Dexposed爲例:

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

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

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

3.基於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包的方式,一旦找到目標類,則停止遍歷

這就給了我們一個想法,有沒有可能把打了補丁的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繼承自共同的父類。顯然,這是最合適的加載器。

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

發佈了173 篇原創文章 · 獲贊 41 · 訪問量 17萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章