Android熱修復(fù)原理及實現(xiàn)

前言

自己之前也做過插件化換膚,涉及到的是插件資源文件的加載;最近看到同事培訓(xùn)的插件化涉及到具體代碼的加載;想自己了解一下断楷,就先從最常用的熱修復(fù)開始看起,由于剛開始接觸相關(guān)的概念崭别,理解也不是很深冬筒,但是總體看下來還是比較簡單的,這里記錄一下自己的理解茅主;

熱修復(fù)的應(yīng)用場景

熱修復(fù)就是在APP上線以后舞痰,如果突然發(fā)現(xiàn)有缺陷了,如果重新走發(fā)布流程可能時間比較長诀姚,重新安裝APP用戶體驗也不會太好响牛;熱修復(fù)就是通過發(fā)布一個插件,使APP運(yùn)行的時候加載插件里面的代碼,從而解決缺陷呀打,并且對于用戶來說是無感的(用戶也可能需要重啟一下APP)矢赁。

熱修復(fù)的原理

先說結(jié)論吧,就是將補(bǔ)丁 dex 文件放到 dexElements 數(shù)組靠前位置贬丛,這樣在加載 class 時撩银,優(yōu)先找到補(bǔ)丁包中的 dex 文件,加載到 class 之后就不再尋找瘫寝,從而原來的 apk 文件中同名的類就不會再使用蜒蕾,從而達(dá)到修復(fù)的目的

理解這個原理稠炬,需要了解一下Android的代碼加載的機(jī)制焕阿;

Android運(yùn)行流程

簡單來講整體流程是這樣的:
1、Android程序編譯的時候首启,會將.java文件編譯時.class文件
2暮屡、然后將.class文件打包為.dex文件
3、然后Android程序運(yùn)行的時候毅桃,Android的Dalvik/ART虛擬機(jī)就加載.dex文件
4褒纲、加載其中的.class文件到內(nèi)存中來使用

類加載器

負(fù)責(zé)加載這些.class文件的就是類加載器(ClassLoader),APP啟動的時候钥飞,會創(chuàng)建一個自己的ClassLoader實例莺掠,我們可以通過下面的代碼拿到當(dāng)前的ClassLoader

ClassLoader classLoader = getClassLoader();
Log.i(TAG, "[onCreate] classLoader" + ":" + classLoader.toString());

ClassLoader加載類的方法就是loadClass可以看一下源碼,是通過雙親委派模型(Parents Delegation Model)读宙,它首先不會自己去嘗試加載這個類彻秆, 而是把這個請求委派給父類加載器去完成,當(dāng)父加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類) 時结闸, 子加載器才會嘗試自己去完成加載唇兑,最后是調(diào)用自己的findClass方法完成的

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            return c;
    }

ClassLoader是一個抽象類,通過打印可以看出來當(dāng)前的ClassLoader是一個PathClassLoader桦锄;看一下PathClassLoader的構(gòu)造函數(shù)扎附,可以看出,需要傳入一個dexPath也就是dex包的路徑结耀,和父類加載器留夜;

    //dexPath 包含 dex 的 jar 文件或 apk 文件的路徑集,多個以文件分隔符分隔图甜,默認(rèn)是“:”
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }

PathClassLoader是BaseDexClassLoader的子類香伴,除此之外BaseDexClassLoader還有一個子類是DexClassLoader,optimizedDirectory用來緩存優(yōu)化的 dex 文件的路徑具则,即從 apk 或 jar 文件中提取出來的 dex 文件即纲;

    public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }

這兩個的區(qū)別,網(wǎng)上的答案是

1博肋、DexClassLoader可以加載jar/apk/dex低斋,可以從SD卡中加載未安裝的apk
2蜂厅、PathClassLoader只能加載系統(tǒng)中已經(jīng)安裝過的apk

從這個答案可以知道,我們想要加載更新的插件膊畴,肯定是使用 DexClassLoader掘猿;但是有點離譜的是其實我用兩個都能成功,也許我加載的插件包名這些都和原APP一致導(dǎo)致的吧唇跨。

類加載器的運(yùn)行流程

具體的實現(xiàn)都在BaseDexClassLoader里面稠通,看一下里面的實現(xiàn)(源碼看不了,網(wǎng)上搜一下)买猖,下面是一個構(gòu)造方法

public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
    super(parent);
    this.originalPath = dexPath;
    this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}

構(gòu)造方法創(chuàng)建了一個DexPathLis改橘,里面解析了dex文件的路徑,并將解析的dex文件都存在this.dexElements里面


public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) {
…
    //將解析的dex文件都存在this.dexElements里面
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory);
}
 
 //解析dex文件
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory) {
    ArrayList<Element> elements = new ArrayList<Element>();
    for (File file : files) {
        ZipFile zip = null;
        DexFile dex = null;
        String name = file.getName();
        if (name.endsWith(DEX_SUFFIX)) {
            dex = loadDexFile(file, optimizedDirectory);
        } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX) || name.endsWith(ZIP_SUFFIX)) {
            zip = new ZipFile(file);
        }
        ……
        if ((zip != null) || (dex != null)) {
            elements.add(new Element(file, zip, dex));
        }
    } return elements.toArray(new Element[elements.size()]);
}

然后我們再回頭看一下ClassLoader加載類的方法,就是loadClass()玉控,最后調(diào)用findClass方法完成的;BaseDexClassLoader 重寫了該方法飞主,如下

 @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        // 使用pathList對象查找name類
        Class c = pathList.findClass(name, suppressedExceptions);
        return c;
    }

最終是調(diào)用 pathList的findClass方法,看一下方法如下

public Class findClass(String name, List<Throwable> suppressed) {
    // 遍歷從dexPath查詢到的dex和資源Element
    for (Element element : dexElements) {
        DexFile dex = element.dexFile;
        // 如果當(dāng)前的Element是dex文件元素
        if (dex != null) {
            // 使用DexFile.loadClassBinaryName加載類
            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }
    }
    if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
}

結(jié)論

所以整個類加載流程就是

1高诺、類加載器BaseDexClassLoader先將dex文件解析放到pathList到dexElements里面
2碌识、加載類的時候從dexElements里面去遍歷,看哪個dex里面有這個類就去加載虱而,生成class對象

所以我們可以將自己的dex文件加載到dexElements里面筏餐,并且放在前面,加載的時候就可以加載我們插件中的類牡拇,不會加載后面的,從而替換掉原來的class魁瞪。

熱修復(fù)的實現(xiàn)

知道了原理,實現(xiàn)就比較簡單了诅迷,就添加新的dex對象到當(dāng)前APP的ClassLoader對象(也就是BaseDexClassLoader)的pathList里面的dexElements佩番;要添加就要先創(chuàng)建,我們使用DexClassLoader先加載插件罢杉,先生成插件的dexElements趟畏,然后再添加就好了。

當(dāng)然整個過程需要使用反射來實現(xiàn)滩租。除此以外赋秀,常用的兩種方法是使用apk作為插件和使用dex文件作為插件;下面的兩個實現(xiàn)都是對程序中的一個方法進(jìn)行了修改律想,然后分別打了 dex包和apk包猎莲,程序運(yùn)行起來執(zhí)行的方法就是插件里面的方法而不是程序本身的方法;

dex插件

對于dex文件作為插件技即,和之前說的流程完全一致著洼,先將修改了的類進(jìn)行打包成dex包,將dex進(jìn)行加載,插入到dexElements集合的前面即可身笤;打包流程是先將.java文件編譯成.class文件豹悬,然后使用SDK工具打包成dex文件人,然后APP下載液荸,加載即可瞻佛;

dex打包工具

d8 作為獨(dú)立工具納入了 Android 構(gòu)建工具 28.0.1 及更高版本中:C:\Users\hanpei\AppData\Local\Android\Sdk\build-tools\29.0.2\d8.bat;輸入字節(jié)碼可以是 *.class 文件或容器(例如 JAR娇钱、APK 或 ZIP 文件)的任意組合伤柄。您還可以添加 DEX 文件作為 d8 的輸入,以將這些文件合并到 DEX 輸出中

 d8 MyProject/app/build/intermediates/classes/debug/*/*.class

具體的代碼實現(xiàn)

代碼的注釋已經(jīng)很詳細(xì)了文搂,就不再進(jìn)行說明了

//在Application中進(jìn)行替換
public class MApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        //dex作為插件進(jìn)行加載
        dexPlugin();
    }
    ...

  /**
     * dex作為插件加載
     */
    private void dexPlugin(){
        //插件包文件
        File file = new File("/sdcard/FixTest.dex");
        if (!file.exists()) {
            Log.i("MApplication", "插件包不在");
            return;
        }
        try {
            //獲取到 BaseDexClassLoader 的  pathList字段
            // private final DexPathList pathList;
            Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");
            //破壞封裝适刀,設(shè)置為可以調(diào)用
            pathListField.setAccessible(true);
            //拿到當(dāng)前ClassLoader的pathList對象
            Object pathListObj = pathListField.get(getClassLoader());

            //獲取當(dāng)前ClassLoader的pathList對象的字節(jié)碼文件(DexPathList )
            Class<?> dexPathListClass = pathListObj.getClass();
            //拿到DexPathList 的 dexElements字段
            // private final Element[] dexElements;
            Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");
            //破壞封裝细疚,設(shè)置為可以調(diào)用
            dexElementsField.setAccessible(true);

            //使用插件創(chuàng)建 ClassLoader
            DexClassLoader pathClassLoader = new DexClassLoader(file.getPath(), getCacheDir().getAbsolutePath(), null, getClassLoader());
            //拿到插件的DexClassLoader 的 pathList對象
            Object newPathListObj = pathListField.get(pathClassLoader);
            //拿到插件的pathList對象的 dexElements變量
            Object newDexElementsObj = dexElementsField.get(newPathListObj);

            //拿到當(dāng)前的pathList對象的 dexElements變量
            Object dexElementsObj=dexElementsField.get(pathListObj);

            int oldLength = Array.getLength(dexElementsObj);
            int newLength = Array.getLength(newDexElementsObj);
            //創(chuàng)建一個dexElements對象
            Object concatDexElementsObject = Array.newInstance(dexElementsObj.getClass().getComponentType(), oldLength + newLength);
            //先添加新的dex添加到dexElement
            for (int i = 0; i < newLength; i++) {
                Array.set(concatDexElementsObject, i, Array.get(newDexElementsObj, i));
            }
            //再添加之前的dex添加到dexElement
            for (int i = 0; i < oldLength; i++) {
                Array.set(concatDexElementsObject, newLength + i, Array.get(dexElementsObj, i));
            }
            //將組建出來的對象設(shè)置給 當(dāng)前ClassLoader的pathList對象
            dexElementsField.set(pathListObj, concatDexElementsObject);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

apk插件

apk作為插件蔗彤,就是我們重新打了一個新的apk包作為插件川梅,打包很簡單方便疯兼,缺點就是文件大;使用apk的話就沒必要是將dex插入dexElements里面去贫途,直接將之前的dexElements替換就可以了吧彪;

具體的實現(xiàn)

代碼的注釋已經(jīng)很詳細(xì)了,就不再進(jìn)行說明了

  /**
     * apk作為插件加載
     */
    private void apkPlugin() {
        //插件包文件
        File file = new File("/sdcard/FixTest.apk");
        if (!file.exists()) {
            Log.i("MApplication", "插件包不在");
            return;
        }
        try {
            //獲取到 BaseDexClassLoader 的  pathList字段
            // private final DexPathList pathList;
            Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");
            //破壞封裝丢早,設(shè)置為可以調(diào)用
            pathListField.setAccessible(true);
            //拿到當(dāng)前ClassLoader的pathList對象
            Object pathListObj = pathListField.get(getClassLoader());

            //獲取當(dāng)前ClassLoader的pathList對象的字節(jié)碼文件(DexPathList )
            Class<?> dexPathListClass = pathListObj.getClass();
            //拿到DexPathList 的 dexElements字段
            // private final Element[] dexElements姨裸;
            Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");
            //破壞封裝,設(shè)置為可以調(diào)用
            dexElementsField.setAccessible(true);

            //使用插件創(chuàng)建 ClassLoader
            DexClassLoader pathClassLoader = new DexClassLoader(file.getPath(), getCacheDir().getAbsolutePath(), null, getClassLoader());
            //拿到插件的DexClassLoader 的 pathList對象
            Object newPathListObj = pathListField.get(pathClassLoader);
            //拿到插件的pathList對象的 dexElements變量
            Object newDexElementsObj = dexElementsField.get(newPathListObj);
            //將插件的 dexElements對象設(shè)置給 當(dāng)前ClassLoader的pathList對象
            dexElementsField.set(pathListObj, newDexElementsObj);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

總結(jié)

思路還是很清晰的怨酝,主要是要先了解類加載的原理傀缩,整體來講還是比較簡單的;采用類加載方案的主要是以騰訊系為主农猬,包括微信的Tinker赡艰、QQ空間的超級補(bǔ)丁、手機(jī)QQ的QFix斤葱、餓了么的Amigo和Nuwa等等慷垮;也有一些其他的方法來實現(xiàn)熱修復(fù),有空再進(jìn)行總結(jié)分享揍堕。

Github地址: https://github.com/tyhjh/HotFix

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末料身,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子衩茸,更是在濱河造成了極大的恐慌芹血,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異幔烛,居然都是意外死亡隙畜,警方通過查閱死者的電腦和手機(jī)朋其,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門挖藏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人腹泌,你說我怎么就攤上這事乡恕⊙匝” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵傲宜,是天一觀的道長运杭。 經(jīng)常有香客問我,道長函卒,這世上最難降的妖魔是什么辆憔? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮报嵌,結(jié)果婚禮上虱咧,老公的妹妹穿的比我還像新娘。我一直安慰自己锚国,他們只是感情好腕巡,可當(dāng)我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著血筑,像睡著了一般绘沉。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上豺总,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天车伞,我揣著相機(jī)與錄音,去河邊找鬼喻喳。 笑死另玖,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的沸枯。 我是一名探鬼主播日矫,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼绑榴!你這毒婦竟也來了哪轿?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤翔怎,失蹤者是張志新(化名)和其女友劉穎窃诉,沒想到半個月后杨耙,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡飘痛,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年珊膜,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片宣脉。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡车柠,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出塑猖,到底是詐尸還是另有隱情竹祷,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布羊苟,位于F島的核電站塑陵,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏蜡励。R本人自食惡果不足惜令花,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望凉倚。 院中可真熱鬧兼都,春花似錦、人聲如沸占遥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽瓦胎。三九已至,卻和暖如春尤揣,著一層夾襖步出監(jiān)牢的瞬間搔啊,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工北戏, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留负芋,地道東北人。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓嗜愈,卻偏偏與公主長得像旧蛾,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子蠕嫁,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容