Android 熱修復介紹之代碼修復

什么是Android熱修復技術(shù)

簡單來說就是不重新安裝apk的情況下,通過補丁,修復bug


正常開發(fā)流程

熱修復開發(fā)流程

目前主流的熱修復技術(shù)框架

  • 阿里系的: Andfix、Hotfix、Sophix
  • 騰訊系的:QQ空間超級補丁技術(shù)、Qfix、Tinker(微信)
  • 美團系的:Robust
  • 餓了么的:Amigo

關(guān)于熱修復的技術(shù)積淀

  • 最開始 ,是手淘基于Xposed進行了改進,產(chǎn)生了針對Android Dalvik虛擬機運行時的Java method Hook技術(shù)——Dexposed。但是這個方案由于對底層Dalvik結(jié)構(gòu)過于依賴,最終無法兼容Android5.0以后
  • 后來支付寶提出了新的熱修復方案Andfix。Andfix同樣是一種底層結(jié)構(gòu)替換的方案,也達到了運行時生效及時修復的效果,阿里后來對Andfix改進,對相關(guān)業(yè)務(wù)解耦后,推出了阿里百川Hotfix方案,此時的修復已經(jīng)非常的不錯,對代碼修復需求都可以解決,而且全版本兼容,但是問題在于Anfix本身有局限,它只提供代碼層面的修復,對于資源和so庫的修復都還未能實現(xiàn)
  • 最終在2017年Sophix的橫空出世,打破了各家熱修復技術(shù)紛爭的局面。在代碼修復,資源修復,so修復的方面,以及方案的安全性,易用性放慢,sophix都做到了業(yè)界領(lǐng)先

本文重點介紹如何在項目中實現(xiàn)代碼修復

通過類加載機制實現(xiàn)
  • 優(yōu)點:適用性強、修復范圍廣、限制少
  • 缺點:屬于熱修復中的冷修復、需要重啟App
通過底層替換方法實現(xiàn)
  • 優(yōu)點:時效好、不需重啟,即使生效
  • 缺點:受限制較多(需要修改虛擬機字段,如果手機廠商修改了虛擬機…….)

ClassLoader 簡介

對于 Java 程序來說,編寫程序就是編寫類,運行程序也就是運行類(編譯得到的 class 文件),其中起到關(guān)鍵作用的就是類加載器 ClassLoader。說起類加載器我就想到ClassLoader的雙親委托加載機制,接下來就介紹一下類加載的雙親機制

雙親機制

當類加載器收到加載類或資源的請求時,通常都是先委托給父類加載器加載,也就是說只有當父類加載器找不到指定類或資源時,自身才會執(zhí)行實際的類加載過程,具體的加載過程如下:
1 源 ClassLoader 先判斷該 Class 是否已加載,如果已加載,則直接返回 Class,如果沒有則委托給父類加載器。

2 父類加載器判斷是否加載過該 Class,如果已加載,則直接返回 Class,如果沒有則委托給祖父類加載器。

3 依此類推,直到始祖類加載器(引用類加載器)。

4 始祖類加載器判斷是否加載過該 Class,如果已加載,則直接返回 Class,如果沒有則嘗試從其對應(yīng)的類路徑下尋找 class 字節(jié)碼文件并載入。如果載入成功,則直接返回 Class,如果載入失敗,則委托給始祖類加載器的子類加載器。

5 始祖類加載器的子類加載器嘗試從其對應(yīng)的類路徑下尋找 class 字節(jié)碼文件并載入。如果載入成功,則直接返回 Class,如果載入失敗,則委托給始祖類加載器的孫類加載器。

6 依此類推,直到源 ClassLoader。

7 源 ClassLoader 嘗試從其對應(yīng)的類路徑下尋找 class 字節(jié)碼文件并載入。如果載入成功,則直接返回 Class,如果載入失敗,源 ClassLoader 不會再委托其子類加載器,而是拋出異常。

Android 中的ClassLoader

Android 的 Dalvik/ART 虛擬機如同標準 Java 的 JVM 虛擬機一樣,也是同樣需要加載 class 文件到內(nèi)存中來使用,但是在 ClassLoader 的加載細節(jié)上會有略微的差別。

Android的dex文件
Android 應(yīng)用打包成 apk 文件時,class 文件會被打包成一個或者多個 dex 文件,Android 中的 Dalvik/ART 無法像 JVM 那樣 直接 加載 class 文件和 jar 文件中的 class,需要通過 dx 工具來優(yōu)化轉(zhuǎn)換成 Dalvik byte code 才行,只能通過 dex 或者 包含 dex 的jar、apk 文件來加載(注意 odex 文件后綴可能是 .dex 或 .odex,也屬于 dex 文件),因此 Android 中的 ClassLoader 工作就交給了 BaseDexClassLoader 來處理。

如何通過類加載機制實現(xiàn)

首先需要認識BaseDexClassLoader、PathClassLoaderDexClassLoader

  • PathClassLoader:系統(tǒng)運作,app運行時用于加載app所有需要的類。PathClassLoader 里面除了這 2 個構(gòu)造方法以外就沒有其他的代碼了,具體的實現(xiàn)都是在 BaseDexClassLoader 里面,其 dexPath 比較受限制,一般是已經(jīng)安裝應(yīng)用的 apk 文件路徑

  • DexClassLoader:程序員運作,可以通過它加載我們想加載的資源,一般包括這么幾種:jar、dex、apk等。

  • BaseDexClassLoader:熱修復中的大Boss,PathClassLoader和DexClassLoader均繼承自BaseDexClassLoader,PathClassLoader和DexClassLoader的重要方法均在其父類BaseDexClassLoader中。(因此就需要從BaseDexClassLoader入手)。

對比 PathClassLoader 只能加載已經(jīng)安裝應(yīng)用的 dex 或 apk 文件,DexClassLoader 則沒有此限制,可以從 SD 卡上加載包含 class.dex 的 .jar 和 .apk 文件,這也是插件化和熱修復的基礎(chǔ),在不需要安裝應(yīng)用的情況下,完成需要使用的 dex 的加載。

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;
    }

通過源碼可以看到,BaseDexClassLoader通過pathList.findClass查找類的,這里出現(xiàn)一個 大Boss “PathList
PathList:中保存類所有dex文件和信息,看一下它是怎么查找類的
PathList源碼:

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從dexElements中查找類,如果clazz != null直接return class,這就是我們可以利用的地方,從源碼看,dexElements應(yīng)該是個數(shù)組或者集合,設(shè)想:我們是不是可以把我們修復bug后的xx類,打包成dex,插入到dexElements的最前面,這樣,系統(tǒng)通過PathClassLoader,查找bug類的時候,就會下找到我們的修復bug的xx類,然后直接返回,不去管后面有bug的那個xx類,達到熱修復的功能

理一下我們熱修復的方案
  • 修復有bug的類,生成dex補丁包;

  • 通過反射機制得到PathClassLoader的成員變量PathList字段(DexPathList的屬性)(通過上面分析知道,PathList是PathClassLoader父類BaseDexClasLoader中的)

  • 然后再反射PathList獲取它的dexElements字段(是一個存放dex的Element數(shù)組)

  • 將我們生成的dex補丁包,插入到dexElements的數(shù)組的最前端

項目中的實現(xiàn)

實現(xiàn)步驟

  • 編寫改變前的app

  • 編寫熱修復需要重寫生成的類

  • 生成dex補丁包,并放到服務(wù)器

  • 編寫補丁檢測和下載代碼

  • 編寫修復補丁代碼(即用反射拿到dexElements數(shù)組,把dex放到有問題的類之前)

如何生成dex補丁包

用class文件生成”001dex”補丁
android在sdk/build-tools/文件件下提供了”dx”命令工具,幫助我們將class文件生成dex文件

生成方式如下:

dx –dex –output=<要生成的文件> <’class’文件路徑>

例如:
dx –dex –output=001.dex …MainAtvity …Actvity2.class …People.class

核心代碼

/**
 * 加載并安裝補丁
 * @type {[type]}
 */
private void loadPatch(File file){
        Log.d(TAG, file.getAbsolutePath()) ;
        if(file.exists()){
            Log.d(TAG,"文件存在...") ;
        }else{
            Log.d(TAG, "文件不存在...") ;
        }
        //獲取系統(tǒng)PathClassLoader
        PathClassLoader pLoader = (PathClassLoader) context.getClassLoader();
        //獲取PathClassLoader中的PathList
        Object pPathList = getPathList(pLoader) ;
        if(pPathList == null){
            Log.d(TAG, "get PathClassLoader pathlist failed...") ;
            return ;
        }
        //加載補丁
        DexClassLoader dLoader = new DexClassLoader(file.getAbsolutePath(),optPath, null, pLoader) ;
        //獲取DexClassLoader的pathLit,即BaseDexClassLoader中的pathList
        Object dPathList = getPathList(dLoader) ;
        if(dPathList == null){
            Log.d(TAG, "get DexClassLoader pathList failed...") ;
            return ;
        }
        //獲取PathList和DexClassLoader的DexElements
        Object pElements = getElements(pPathList) ;
        Object dElements = getElements(dPathList) ;

        //將補丁dElements[]插入系統(tǒng)pElements[]的最前面
        Object newElements = insertElements(pElements, dElements) ;
        if(newElements == null){
            Log.d(TAG, "patch insert failed...") ;
            return ;
        }
        //用插入補丁后的新Elements[]替換系統(tǒng)Elements[]
        try {
            Field fElements = pPathList.getClass().getDeclaredField("dexElements") ;
            fElements.setAccessible(true);
            fElements.set(pPathList, newElements);
        } catch (Exception e) {
            e.printStackTrace();
            Log.d(TAG, "fixed failed....") ;
            return ;
        }
    }

    /**
     * 將補丁插入系統(tǒng)DexElements[]最前端,生成一個新的DexElements[]
     * @param pElements
     * @param dElements
     * @return
     */
    private Object insertElements(Object pElements, Object dElements){
        //判斷是否為數(shù)組
        if(pElements.getClass().isArray() && dElements.getClass().isArray()){
            //獲取數(shù)組長度
            int pLen = Array.getLength(pElements) ;
            int dLen = Array.getLength(dElements) ;
            //創(chuàng)建新數(shù)組
            Object newElements = Array.newInstance(pElements.getClass().getComponentType(), pLen+dLen) ;
            //循環(huán)插入
            for(int i=0; i<pLen+dLen;i++){
                if(i<dLen){
                    Array.set(newElements, i, Array.get(dElements, i));
                }else{
                    Array.set(newElements, i, Array.get(pElements, i-dLen)) ;
                }
            }
            return newElements ;
        }
        return null ;
    }

    /**
     *  獲取DexElements
     * @param object
     * @return
     */
    private Object getElements(Object object){
        try {
            Class<?> c = object.getClass() ;
            Field fElements = c.getDeclaredField("dexElements") ;
            fElements.setAccessible(true);
            Object obj = fElements.get(object) ;
            return obj ;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null ;
    }

    /**
     * 通過反射機制獲取PathList
     * @param loader
     * @return
     */
    private Object getPathList(BaseDexClassLoader loader){
        try {
            Class<?> c = Class.forName("dalvik.system.BaseDexClassLoader") ;
            //獲取成員變量pathList
            Field fPathList = c.getDeclaredField("pathList") ;
            //抑制jvm檢測訪問權(quán)限
            fPathList.setAccessible(true);
            //獲取成員變量pathList的值
            Object obj = fPathList.get(loader) ;
            return obj ;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null ;
    }
最后編輯于
?著作權(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)容