以下內(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ǔ)丁原理)如下圖所示:
這里涉及到到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文件的類椎木。
所以QQ空間熱修復(fù)方案正是基于ClassLoader的這個(gè)原理,把修復(fù)后的類打包到一個(gè)dex(path.dex)中去博烂,然后把這個(gè)dex插入到Elements的最前面去香椎。
步驟
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的目的
缺點(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)插入代碼
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ù)栅干。