QQ空間熱修復(fù)原理和注意點(diǎn)

以下內(nèi)容摘錄自Android熱修復(fù)學(xué)習(xí)之旅開(kāi)篇——熱修復(fù)概述
參考:
從Java類加載初始化到Android熱修復(fù)

QQ空間熱修復(fù)補(bǔ)丁技術(shù)

原理

QQ空間的熱修復(fù)方案是基于dex分包的基礎(chǔ)之上的,簡(jiǎn)單來(lái)說(shuō)就是把bug方法修復(fù)之后,然后重新生成一個(gè)dex看靠,從服務(wù)器下發(fā)以后赚哗,將其插入到dexElements前面,讓虛擬機(jī)去加載修復(fù)后的方法纫溃。(關(guān)于dex分包相關(guān)內(nèi)容請(qǐng)參考Android Dex分包方案和熱補(bǔ)丁原理)如下圖所示:

image.png

這里涉及到到ClassLoad的原理父能,當(dāng)一個(gè)類被加載以后,如果后面再出現(xiàn)相同的類就不會(huì)再加載了盼理。這就是補(bǔ)丁熱修復(fù)最基本的原理。
但是采用這種方案俄删,有一個(gè)明顯的問(wèn)題宏怔,那就是當(dāng)兩個(gè)調(diào)用關(guān)系的類不在同一個(gè)dex里時(shí),就會(huì)產(chǎn)生異常報(bào)錯(cuò)畴椰。發(fā)生異常的原因是臊诊,在apk安裝時(shí),虛擬機(jī)會(huì)對(duì)classes.dex進(jìn)行優(yōu)化斜脂,變成odex文件抓艳,然后才會(huì)執(zhí)行。在這個(gè)過(guò)程中帚戳,會(huì)進(jìn)行類的verify的驗(yàn)證工作玷或,如果調(diào)用關(guān)系的類都在同一個(gè)dex中的話就會(huì)被打上CLASS_ISPREVERIFIED的標(biāo)志,然后才會(huì)寫入odex文件片任。

如果使用這種方案時(shí)偏友,必須要避免類被打上CLASS_ISPREVERIFIED標(biāo)記,具體的做法就是在每一個(gè)類的構(gòu)造函數(shù)中單獨(dú)引用一個(gè)在另外dex中的類对供。

我們通過(guò)Android類加載基礎(chǔ)之ClassLoder的分析已經(jīng)知道位他,無(wú)論是PathClassLoader還是DexClassLoader最終調(diào)用的都是BaseDexClassLoader里的方法,而且我們加載完外部的n個(gè)dex以后會(huì)被轉(zhuǎn)換為Element[]數(shù)組存儲(chǔ)在DexPathList中产场。PathClassLoader和DexClassLoader的差別就是DexClassLoader可以加載外部的dex而PathClassLoader只能加載已經(jīng)安裝了的內(nèi)部dex鹅髓,其中PathClassLoader 在應(yīng)用啟動(dòng)時(shí)創(chuàng)建,從 data/app/… 安裝目錄下加載 apk 文件京景。
一個(gè)ClassLoader可以包含多個(gè)dex文件窿冯,每個(gè)dex文件就是一個(gè)Element,多個(gè)dex文件排列成一個(gè)有序的數(shù)組dexElements醋粟,當(dāng)查找某個(gè)類的時(shí)候靡菇,會(huì)按順序遍歷dex文件重归,然后從當(dāng)前遍歷的dex文件中找類,如果找到則直接返回厦凤,如果找不到則從下一個(gè)dex文件繼續(xù)查找鼻吮。
理論上,如果在不同的dex中有相同的類存在较鼓,那么會(huì)優(yōu)先選擇排在前面的dex文件的類椎木。

image.png

所以QQ空間熱修復(fù)方案正是基于ClassLoader的這個(gè)原理,把修復(fù)后的類打包到一個(gè)dex(path.dex)中去博烂,然后把這個(gè)dex插入到Elements的最前面去香椎。


image.png

步驟

1.獲取到當(dāng)前引用的ClassLoader
2.通過(guò)反射獲取到它的DexPathList屬性對(duì)象pathList
3.通過(guò)反射調(diào)用pathList的dexElements方法把path.dex轉(zhuǎn)化為Element數(shù)組
4.兩個(gè)Element數(shù)組進(jìn)行合并,把path.dex放到數(shù)組的最前面去
5.加載合并后行的Element數(shù)組禽篱,達(dá)到修復(fù)bug的目的

image.png

缺點(diǎn)

1.不支持即時(shí)生效畜伐,必須通過(guò)重啟才能生效
2.path.dex是用來(lái)存儲(chǔ)修復(fù)的類,應(yīng)用啟動(dòng)時(shí)躺率,就要加載path.dex,當(dāng)修復(fù)的類到了一定的數(shù)量的時(shí)候玛界,就會(huì)出現(xiàn)加載時(shí)間過(guò)長(zhǎng),造成應(yīng)用啟動(dòng)卡頓
3.在ART模式下悼吱,如果類結(jié)構(gòu)發(fā)生了改變慎框,就會(huì)出現(xiàn)內(nèi)存錯(cuò)亂。為了解決這個(gè)問(wèn)題后添,就必須把所有相關(guān)的調(diào)用類笨枯、父類、子類等等全部加載到path.dex中遇西,導(dǎo)致補(bǔ)丁包異常的大馅精,進(jìn)一步增加了應(yīng)用的啟動(dòng)時(shí)間

CLASS_ISPREVERIFIED的問(wèn)題

采用dex分包方案會(huì)遇到的問(wèn)題,也就是CLASS_ISPREVERIFIED努溃,簡(jiǎn)單來(lái)說(shuō)就是:在虛擬機(jī)啟動(dòng)的時(shí)候硫嘶,當(dāng)verify選項(xiàng)被打開(kāi)的時(shí)候,如果static方法梧税、private方法沦疾、構(gòu)造方法等,其中的直接引用(第一層關(guān)系)到的類都在同一個(gè)dex文件中第队,那么該類就會(huì)被打上CLASS_ISPREVERIFIED標(biāo)志哮塞。
注意:這里避免被打上CLASS_ISPREVERIFIED標(biāo)志的類似引用者類,而不是被引用者凳谦。也就是說(shuō)假設(shè)你的app里面有個(gè)類叫做AClass忆畅,在其內(nèi)部引用了BClass。發(fā)布過(guò)程中發(fā)現(xiàn)BClass有編寫錯(cuò)誤尸执,那么想要發(fā)布一個(gè)新的BClass類家凯,那么你就要阻止AClass這個(gè)類被打上CLASS_ISPREVERIFIED標(biāo)志缓醋。也就是說(shuō),你在生成apk之前绊诲,就需要阻止相關(guān)類打上CLASS_ISPREVERIFIED的標(biāo)志了送粱。如何阻止,簡(jiǎn)單來(lái)說(shuō)掂之,就是讓AClass在構(gòu)造方法中抗俄,去直接引用別的dex文件中的類即可,比如:C.dex中的CClass世舰。
總結(jié):
1.動(dòng)態(tài)改變BaseDexClassLoader對(duì)象間接引用的 dexElements
2.在app打包的時(shí)候动雹,阻止相關(guān)類去打上CLASS_ISPREVERIFIED標(biāo)志。

關(guān)鍵代碼實(shí)現(xiàn)

采用QQ空間的熱修復(fù)方案而實(shí)現(xiàn)的開(kāi)源熱修復(fù)框架就是HotFix,說(shuō)到了使用dex分包方案會(huì)遇到CLASS_ISPREVERIFIED問(wèn)題跟压,而解決方案就是在dx工具執(zhí)行之前胰蝠,將所有的class文件,進(jìn)行修改裆馒,再其構(gòu)造中添加System.out.println(dodola.hackdex.AntilazyLoad.class)姊氓,然后繼續(xù)打包的流程。注意:AntilazyLoad.class這個(gè)類是獨(dú)立在hack.dex中喷好。

dex分包方案實(shí)現(xiàn)需要關(guān)注以下問(wèn)題:

1.如何解決CLASS_ISPREVERIFIED問(wèn)題
2.如何將修復(fù)的.dex文件插入到dexElements的最前面

CLASS_ISPREVERIFIED問(wèn)題

在老版的Gradle中我們通過(guò)以下代碼關(guān)聯(lián)task并執(zhí)行插件來(lái)動(dòng)態(tài)插入代碼

image.png

image.png

PatchClass中的代碼比較簡(jiǎn)單我就不分析了,主要用到了javassist技術(shù)读跷,感興趣的朋友可以去查找相關(guān)的資料梗搅。
Gradle的更新速度很快,當(dāng)我們的AndroidStudio升級(jí)以后效览,系統(tǒng)已經(jīng)提供了更好的api來(lái)操作代碼在編譯過(guò)程中的回調(diào)无切,這就是Transform api,該興趣的小伙伴可以參考我以前寫的文章:編寫最基本的Gradle插件
經(jīng)過(guò)上面的代碼丐枉,我們已經(jīng)解決了CLASS_ISPREVERIFIED的問(wèn)題

將修復(fù)的.dex文件插入dexElements

尋找class其實(shí)就是遍歷dexElements哆键,然后我們的AntilazyLoad.class其實(shí)并不包含在apk的classes.dex中,并且根據(jù)上面描述的需要瘦锹,我們需要將AntilazyLoad.class這個(gè)類打成獨(dú)立的hack_dex.jar籍嘹,注意不是普通的jar,而是經(jīng)過(guò)dx工具進(jìn)行轉(zhuǎn)化后的弯院。具體做法如下:

jar cvf hack.jar dodola/hackdex/*
dx --dex --output hack_dex.jar hack.jar

還記得之前我們將所有的類的構(gòu)造方法中都引用了AntilazyLoad.class辱士,所以我們需要把hack_dex.jar插入到dexElements,而在hotfix中听绳,就是在Application中完成這個(gè)操作的颂碘。

        File dexPath = new File(getDir("dex", Context.MODE_PRIVATE), "hackdex_dex.jar");
        //從assets中讀取文件被寫入到指定文件中去
        Utils.prepareDex(this.getApplicationContext(), dexPath, "hackdex_dex.jar");
        //通過(guò)反射去修改dexElements數(shù)組
        HotFix.patch(this, dexPath.getAbsolutePath(), "dodola.hackdex.AntilazyLoad");
        try {
            this.getClassLoader().loadClass("dodola.hackdex.AntilazyLoad");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

HotFix#patch

public static void patch(Context context, String patchDexFile, String patchClassName) {
        if (patchDexFile != null && new File(patchDexFile).exists()) {
            try {
                if (hasLexClassLoader()) {
                    injectInAliyunOs(context, patchDexFile, patchClassName);
                } else if (hasDexClassLoader()) {
                    injectAboveEqualApiLevel14(context, patchDexFile, patchClassName);
                } else {
                    injectBelowApiLevel14(context, patchDexFile, patchClassName);
                }
            } catch (Throwable th) {
            }
        }
    }

根據(jù)不同的平臺(tái)然后分別注入到不同平臺(tái)下的dexElements中。這里我們只分析api
14 以上的平臺(tái)椅挣,其他平臺(tái)大家自己去分析头岔。其實(shí)原理都是差不多的塔拳。

private static void injectAboveEqualApiLevel14(Context context, String dexPath, String dexClassName)
        throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        //得到當(dāng)前的PathClassLoader
        PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
        //將老的dexElements和pathDexElements進(jìn)行組合生出新的dexElements
        Object a = combineArray(getDexElements(getPathList(pathClassLoader)),
            getDexElements(getPathList(
                new DexClassLoader(dexPath, context.getDir("dex", 0).getAbsolutePath(), dexPath, context.getClassLoader()))));
        //拿到DexPathList對(duì)象
        Object a2 = getPathList(pathClassLoader);
        //將DexPathList實(shí)例中的dexElements成員替換為合并后的dexElements
        setField(a2, a2.getClass(), "dexElements", a);
        //加載指定的類
        pathClassLoader.loadClass(dexClassName);
    }

好了,經(jīng)過(guò)上面的分析峡竣,我們已經(jīng)將hack_dex.jar成功的插入到dexElements的最前面了蝙斜,而補(bǔ)丁插入的過(guò)程也和hack_dex.jar的插入流程是一致。

總結(jié)

QQ熱修復(fù)步驟

1.發(fā)現(xiàn)某個(gè)類中存在bug
2.創(chuàng)建一個(gè)相同的類解決bug澎胡,并通過(guò)javassist技術(shù)解決CLASS_ISPREVERIFIED的問(wèn)題孕荠,然后下發(fā)到指定的客戶端
3.app啟動(dòng)時(shí)會(huì)創(chuàng)建PathClassLoader,掃描指定目錄下的dex文件并保存到DexPathList的dexElements數(shù)組中攻谁。
4.Application#onCreate中查找是否有path.dex文件稚伍,如果沒(méi)有則通過(guò)網(wǎng)絡(luò)下載,保存到assets中戚宦,然后拷貝到app的指定目錄下个曙。如果存在path.dex,則創(chuàng)建DexClassLoader加載它受楼,然后得到它的dexElements數(shù)組垦搬,與PathClassloader中的dexElements數(shù)組進(jìn)行合并(插入到頭部),通過(guò)反射將新生成的數(shù)組注入到原來(lái)的dexElements中艳汽,從而完成bug類的替換猴贰。
5.當(dāng)我們?cè)诔跏蓟鹊腷ug類的時(shí)候,會(huì)從新生成dexElements中查找河狐,由于雙親委托米绕,當(dāng)我們已經(jīng)找到了新類的時(shí)候,他就不會(huì)再去查找原先老的bug類馋艺,所以此時(shí)的對(duì)象舊已經(jīng)完成了bug的修復(fù)栅干。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市捐祠,隨后出現(xiàn)的幾起案子碱鳞,更是在濱河造成了極大的恐慌,老刑警劉巖踱蛀,帶你破解...
    沈念sama閱讀 222,590評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件窿给,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡星岗,警方通過(guò)查閱死者的電腦和手機(jī)填大,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,157評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)俏橘,“玉大人允华,你說(shuō)我怎么就攤上這事。” “怎么了靴寂?”我有些...
    開(kāi)封第一講書人閱讀 169,301評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵磷蜀,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我百炬,道長(zhǎng)褐隆,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 60,078評(píng)論 1 300
  • 正文 為了忘掉前任剖踊,我火速辦了婚禮庶弃,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘德澈。我一直安慰自己歇攻,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,082評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布梆造。 她就那樣靜靜地躺著缴守,像睡著了一般。 火紅的嫁衣襯著肌膚如雪镇辉。 梳的紋絲不亂的頭發(fā)上屡穗,一...
    開(kāi)封第一講書人閱讀 52,682評(píng)論 1 312
  • 那天,我揣著相機(jī)與錄音忽肛,去河邊找鬼村砂。 笑死,一個(gè)胖子當(dāng)著我的面吹牛麻裁,可吹牛的內(nèi)容都是我干的箍镜。 我是一名探鬼主播,決...
    沈念sama閱讀 41,155評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼煎源,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了香缺?” 一聲冷哼從身側(cè)響起手销,我...
    開(kāi)封第一講書人閱讀 40,098評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎图张,沒(méi)想到半個(gè)月后锋拖,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,638評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡祸轮,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,701評(píng)論 3 342
  • 正文 我和宋清朗相戀三年兽埃,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片适袜。...
    茶點(diǎn)故事閱讀 40,852評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡柄错,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情售貌,我是刑警寧澤给猾,帶...
    沈念sama閱讀 36,520評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站颂跨,受9級(jí)特大地震影響敢伸,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜恒削,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,181評(píng)論 3 335
  • 文/蒙蒙 一池颈、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧钓丰,春花似錦躯砰、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,674評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至则北,卻和暖如春矿微,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背尚揣。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,788評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工涌矢, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人快骗。 一個(gè)月前我還...
    沈念sama閱讀 49,279評(píng)論 3 379
  • 正文 我出身青樓娜庇,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親方篮。 傳聞我的和親對(duì)象是個(gè)殘疾皇子名秀,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,851評(píng)論 2 361