安卓App熱補(bǔ)丁動(dòng)態(tài)修復(fù)技術(shù)介紹

1.背景

當(dāng)一個(gè)App發(fā)布之后,突然發(fā)現(xiàn)了一個(gè)嚴(yán)重bug需要進(jìn)行緊急修復(fù),這時(shí)候公司各方就會(huì)忙得焦頭爛額:重新打包App、測(cè)試、向各個(gè)應(yīng)用市場(chǎng)和渠道換包、提示用戶(hù)升級(jí)、用戶(hù)下載、覆蓋安裝。有時(shí)候僅僅是為了修改了一行代碼,也要付出巨大的成本進(jìn)行換包和重新發(fā)布。

這時(shí)候就提出一個(gè)問(wèn)題:有沒(méi)有辦法以補(bǔ)丁的方式動(dòng)態(tài)修復(fù)緊急Bug,不再需要重新發(fā)布App,不再需要用戶(hù)重新下載,覆蓋安裝?

雖然Android系統(tǒng)并沒(méi)有提供這個(gè)技術(shù),但是很幸運(yùn)的告訴大家,答案是:可以,我們QQ空間提出了熱補(bǔ)丁動(dòng)態(tài)修復(fù)技術(shù)來(lái)解決以上這些問(wèn)題。

2.實(shí)際案例

空間Android獨(dú)立版5.2發(fā)布后,收到用戶(hù)反饋,結(jié)合版無(wú)法跳轉(zhuǎn)到獨(dú)立版的訪(fǎng)客界面,每天都較大的反饋。在以前只能緊急換包,重新發(fā)布。成本非常高,也影響用戶(hù)的口碑。最終決定使用熱補(bǔ)丁動(dòng)態(tài)修復(fù)技術(shù),向用戶(hù)下發(fā)Patch,在用戶(hù)無(wú)感知的情況下,修復(fù)了外網(wǎng)問(wèn)題,取得非常好的效果。

3.解決方案

該方案基于的是android dex分包方案的,關(guān)于dex分包方案,網(wǎng)上有幾篇解釋了,所以這里就不再贅述,具體可以看這里https://m.oschina.net/blog/308583。

簡(jiǎn)單的概括一下,就是把多個(gè)dex文件塞入到app的classloader之中,但是android dex拆包方案中的類(lèi)是沒(méi)有重復(fù)的,如果classes.dex和classes1.dex中有重復(fù)的類(lèi),當(dāng)用到這個(gè)重復(fù)的類(lèi)的時(shí)候,系統(tǒng)會(huì)選擇哪個(gè)類(lèi)進(jìn)行加載呢?

讓我們來(lái)看看類(lèi)加載的代碼:

一個(gè)ClassLoader可以包含多個(gè)dex文件,每個(gè)dex文件是一個(gè)Element,多個(gè)dex文件排列成一個(gè)有序的數(shù)組dexElements,當(dāng)找類(lèi)的時(shí)候,會(huì)按順序遍歷dex文件,然后從當(dāng)前遍歷的dex文件中找類(lèi),如果找類(lèi)則返回,如果找不到從下一個(gè)dex文件繼續(xù)查找。

理論上,如果在不同的dex中有相同的類(lèi)存在,那么會(huì)優(yōu)先選擇排在前面的dex文件的類(lèi),如下圖:

在此基礎(chǔ)上,我們構(gòu)想了熱補(bǔ)丁的方案,把有問(wèn)題的類(lèi)打包到一個(gè)dex(patch.dex)中去,然后把這個(gè)dex插入到Elements的最前面,如下圖:

好,該方案基于第二個(gè)拆分dex的方案,方案實(shí)現(xiàn)如果懂拆分dex的原理的話(huà),大家應(yīng)該很快就會(huì)實(shí)現(xiàn)該方案,如果沒(méi)有拆分dex的項(xiàng)目的話(huà),可以參考一下谷歌的multidex方案實(shí)現(xiàn)。然后在插入數(shù)組的時(shí)候,把補(bǔ)丁包插入到最前面去。

好,看似問(wèn)題很簡(jiǎn)單,輕松的搞定了,讓我們來(lái)試驗(yàn)一下,修改某個(gè)類(lèi),然后打包成dex,插入到classloader,當(dāng)加載類(lèi)的時(shí)候出現(xiàn)了(本例中是QzoneActivityManager要被替換):

為什么會(huì)出現(xiàn)以上問(wèn)題呢?

從log的意思上來(lái)講,ModuleManager引用了QzoneActivityManager,但是發(fā)現(xiàn)這這兩個(gè)類(lèi)所在的dex不在一起,其中:

1. ModuleManager在classes.dex中

2. QzoneActivityManager在patch.dex中

結(jié)果發(fā)生了錯(cuò)誤。

這里有個(gè)問(wèn)題,拆分dex的很多類(lèi)都不是在同一個(gè)dex內(nèi)的,怎么沒(méi)有問(wèn)題?

讓我們搜索一下拋出錯(cuò)誤的代碼所在,嘿咻嘿咻,找到了一下代碼:

從代碼上來(lái)看,如果兩個(gè)相關(guān)聯(lián)的類(lèi)在不同的dex中就會(huì)報(bào)錯(cuò),但是拆分dex沒(méi)有報(bào)錯(cuò)這是為什么,原來(lái)這個(gè)校驗(yàn)的前提是:

如果引用者(也就是ModuleManager)這個(gè)類(lèi)被打上了CLASS_ISPREVERIFIED標(biāo)志,那么就會(huì)進(jìn)行dex的校驗(yàn)。那么這個(gè)標(biāo)志是什么時(shí)候被打上去的?讓我們?cè)诶^續(xù)搜索一下代碼,嘿咻嘿咻~~,在DexPrepare.cpp找到了一下代碼:

這段代碼是dex轉(zhuǎn)化成odex(dexopt)的代碼中的一段,我們知道當(dāng)一個(gè)apk在安裝的時(shí)候,apk中的classes.dex會(huì)被虛擬機(jī)(dexopt)優(yōu)化成odex文件,然后才會(huì)拿去執(zhí)行。

虛擬機(jī)在啟動(dòng)的時(shí)候,會(huì)有許多的啟動(dòng)參數(shù),其中一項(xiàng)就是verify選項(xiàng),當(dāng)verify選項(xiàng)被打開(kāi)的時(shí)候,上面doVerify變量為true,那么就會(huì)執(zhí)行dvmVerifyClass進(jìn)行類(lèi)的校驗(yàn),如果dvmVerifyClass校驗(yàn)類(lèi)成功,那么這個(gè)類(lèi)會(huì)被打上CLASS_ISPREVERIFIED的標(biāo)志,那么具體的校驗(yàn)過(guò)程是什么樣子的呢?

此代碼在DexVerify.cpp中,如下:

1. 驗(yàn)證clazz->directMethods方法,directMethods包含了以下方法:

1. static方法

2. private方法

3. 構(gòu)造函數(shù)

2.clazz->virtualMethods

1. 虛函數(shù)=override方法?

概括一下就是如果以上方法中直接引用到的類(lèi)(第一層級(jí)關(guān)系,不會(huì)進(jìn)行遞歸搜索)和clazz都在同一個(gè)dex中的話(huà),那么這個(gè)類(lèi)就會(huì)被打上CLASS_ISPREVERIFIED:

所以為了實(shí)現(xiàn)補(bǔ)丁方案,所以必須從這些方法中入手,防止類(lèi)被打上CLASS_ISPREVERIFIED標(biāo)志。

最終空間的方案是往所有類(lèi)的構(gòu)造函數(shù)里面插入了一段代碼,代碼如下:

if (ClassVerifier.PREVENT_VERIFY) {

System.out.println(AntilazyLoad.class);

}

其中AntilazyLoad類(lèi)會(huì)被打包成單獨(dú)的hack.dex,這樣當(dāng)安裝apk的時(shí)候,classes.dex內(nèi)的類(lèi)都會(huì)引用一個(gè)在不相同dex中的AntilazyLoad類(lèi),這樣就防止了類(lèi)被打上CLASS_ISPREVERIFIED的標(biāo)志了,只要沒(méi)被打上這個(gè)標(biāo)志的類(lèi)都可以進(jìn)行打補(bǔ)丁操作。

然后在應(yīng)用啟動(dòng)的時(shí)候加載進(jìn)來(lái).AntilazyLoad類(lèi)所在的dex包必須被先加載進(jìn)來(lái),不然AntilazyLoad類(lèi)會(huì)被標(biāo)記為不存在,即使后續(xù)加載了hack.dex包,那么他也是不存在的,這樣屏幕就會(huì)出現(xiàn)茫茫多的類(lèi)AntilazyLoad找不到的log。

所以Application作為應(yīng)用的入口不能插入這段代碼。(因?yàn)檩d入hack.dex的代碼是在Application中onCreate中執(zhí)行的,如果在Application的構(gòu)造函數(shù)里面插入了這段代碼,那么就是在hack.dex加載之前就使用該類(lèi),該類(lèi)一次找不到,會(huì)被永遠(yuǎn)的打上找不到的標(biāo)志)

其中:

之所以選擇構(gòu)造函數(shù)是因?yàn)樗辉黾臃椒〝?shù),一個(gè)類(lèi)即使沒(méi)有顯式的構(gòu)造函數(shù),也會(huì)有一個(gè)隱式的默認(rèn)構(gòu)造函數(shù)。

空間使用的是在字節(jié)碼插入代碼,而不是源代碼插入,使用的是javaassist庫(kù)來(lái)進(jìn)行字節(jié)碼插入的。

隱患:

虛擬機(jī)在安裝期間為類(lèi)打上CLASS_ISPREVERIFIED標(biāo)志是為了提高性能的,我們強(qiáng)制防止類(lèi)被打上標(biāo)志是否會(huì)影響性能?這里我們會(huì)做一下更加詳細(xì)的性能測(cè)試.但是在大項(xiàng)目中拆分dex的問(wèn)題已經(jīng)比較嚴(yán)重,很多類(lèi)都沒(méi)有被打上這個(gè)標(biāo)志。

如何打包補(bǔ)丁包:

1. 空間在正式版本發(fā)布的時(shí)候,會(huì)生成一份緩存文件,里面記錄了所有class文件的md5,還有一份mapping混淆文件。

2. 在后續(xù)的版本中使用-applymapping選項(xiàng),應(yīng)用正式版本的mapping文件,然后計(jì)算編譯完成后的class文件的md5和正式版本進(jìn)行比較,把不相同的class文件打包成補(bǔ)丁包。

備注:該方案現(xiàn)在也應(yīng)用到我們的編譯過(guò)程當(dāng)中,編譯不需要重新打包dex,只需要把修改過(guò)的類(lèi)的class文件打包成patch dex,然后放到sdcard下,那么就會(huì)讓改變的代碼生效。

尊重原作者,轉(zhuǎn)自:
https://mp.weixin.qq.com/s?__biz=MzI1MTA1MzM2Nw==&mid=400118620&idx=1&sn=b4fdd5055731290eef12ad0d17f39d4a&scene=1&srcid=1106Imu9ZgwybID13e7y2nEi#wechat_redirect

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容