動(dòng)態(tài)補(bǔ)丁修復(fù)(iOS,Android)

原文鏈接:公眾號(hào):QQ空間終端開(kāi)發(fā)團(tuán)隊(duì)

TPatch動(dòng)態(tài)補(bǔ)丁系統(tǒng)(iOS)

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


TPatch是一套使用JavaScript給iOS打熱補(bǔ)丁的系統(tǒng),能非常有效的解決線上App的Crash和各種問(wèn)題。

1.從何而來(lái)?

對(duì)于每一個(gè)開(kāi)發(fā),從寫(xiě)Hello World開(kāi)始,到使用各種語(yǔ)言,可能都會(huì)遇到各種BUG。有的BUG能快速解決,比如Web側(cè)的,發(fā)個(gè)JS或者Html即可。但是在終端開(kāi)發(fā)中,比如iOS,發(fā)現(xiàn)的線上問(wèn)題往往沒(méi)那么快能解決,換包可能需要Apple短則幾天長(zhǎng)則一周以上的審核,成本很高。有沒(méi)有辦法能快速解決iOS App的線上問(wèn)題?TPatch是其中一種比較好的解決方案。

2.TPatch特點(diǎn)

支持多線程:

使用JS打補(bǔ)丁的天然優(yōu)勢(shì)在于JavaScriptCore是線程安全的,雖然鎖的粒度有點(diǎn)大,并且有些方法的鎖有問(wèn)題(這些在TPatch都已解決)。

支持Block:

JS中的function和OC的Block有很多相似之處。有補(bǔ)丁中定義的function,傳遞到OC,我們會(huì)轉(zhuǎn)成Block,并且Block可以在OC和JS之間傳遞,這點(diǎn)Lua補(bǔ)丁是很難做到。

異步機(jī)制:

由于JavsSciptCore是線程安全的,同時(shí)也帶來(lái)另外一個(gè)問(wèn)題,假如工作線程和主線程都打了補(bǔ)丁,工作線程的補(bǔ)丁耗時(shí)非常嚴(yán)重,這時(shí)候如果主線程補(bǔ)丁開(kāi)始運(yùn)行,就會(huì)被阻塞。TPatch引入了異步機(jī)制,能讓進(jìn)入JSCore的補(bǔ)丁快速返回,異步執(zhí)行,減少補(bǔ)丁之間的影響。

支持在線Reset回滾:

在補(bǔ)丁發(fā)布后,有可能通過(guò)監(jiān)控發(fā)現(xiàn)補(bǔ)丁有問(wèn)題,這時(shí)候用戶(hù)側(cè)的運(yùn)行邏輯已經(jīng)被“污染”。TPatch支持,在補(bǔ)丁后臺(tái)設(shè)置該補(bǔ)丁過(guò)期后,用戶(hù)側(cè)App會(huì)刪掉本地有問(wèn)題的補(bǔ)丁包,并且在線Reset,而不是等App重啟后再恢復(fù),下次重啟可能得好幾天。

調(diào)試方便:

利用JavaScriptCore的天然優(yōu)勢(shì),其內(nèi)部提供了Debug接口。我們可以像調(diào)試App里面的網(wǎng)頁(yè)一樣,使用Mac下的Safari遠(yuǎn)程調(diào)試補(bǔ)丁,斷點(diǎn)、堆棧、異常等一目了然。

精準(zhǔn)投放:

TPatch支持按用戶(hù)、iOS版本、業(yè)務(wù)App版本和Mask標(biāo)記投放。Mask是一個(gè)可擴(kuò)展的bit標(biāo)記,業(yè)務(wù)可以自定義,比如取一位越獄標(biāo)記,或者網(wǎng)絡(luò)標(biāo)記,補(bǔ)丁就可以根據(jù)是否越獄和網(wǎng)絡(luò)標(biāo)記下發(fā)。

3.核心原理

TPatch包括補(bǔ)丁包后臺(tái)系統(tǒng)和終端組件,其核心原理是補(bǔ)丁后臺(tái)根據(jù)補(bǔ)丁配置,下發(fā)一段補(bǔ)丁JS給終端,終端執(zhí)行這段補(bǔ)丁,利用OC Runtime覆蓋有問(wèn)題的方法或者執(zhí)行一段邏輯,修正運(yùn)行時(shí)的邏輯,從而達(dá)到修復(fù)BUG的目的。

4.打補(bǔ)丁流程

1.在補(bǔ)丁后臺(tái)下發(fā)補(bǔ)丁腳本后,首先會(huì)經(jīng)過(guò)iOS7及以上系統(tǒng)自帶的JavaScriptCore.framework把JS補(bǔ)丁執(zhí)行起來(lái),通過(guò)調(diào)用TPatch.js里面的Bridge接口,調(diào)用到OC里面打補(bǔ)丁的方法,打上補(bǔ)丁。

2.當(dāng)業(yè)務(wù)代碼執(zhí)行這段已經(jīng)打了補(bǔ)丁的功能時(shí),不會(huì)是原來(lái)的OC代碼,而是一段JS代碼。JS可以通過(guò)JS引起和OC引擎支持Block、異步執(zhí)行等,并且支持在線Reset回滾。

5.和其他方案對(duì)比

waxPatch:

是使用Lua+Wax打補(bǔ)丁的方案,App需要集成Lua解釋器和Wax框架(接近1M)。不過(guò)waxPatch對(duì)Block不太完善,多線程補(bǔ)丁也可能有問(wèn)題,Wax也已經(jīng)兩年沒(méi)人維護(hù)。相比之下TPatch更加輕量,對(duì)安裝包影響僅200K,功能也更加強(qiáng)大。

JSPatch:

同樣使用JS來(lái)打補(bǔ)丁,和TPatch終端組件核心原理是相似的。不過(guò)JSPatch在實(shí)際的海量產(chǎn)品中運(yùn)用還有不少問(wèn)題沒(méi)解決,比如Block傳遞、多線程Crash等問(wèn)題,TPatch解決了這些問(wèn)題,更加穩(wěn)定,并且支持異步機(jī)制、動(dòng)態(tài)回滾等優(yōu)化特性。


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ā)布。成本非常高,也影響用戶(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的原理的話,大家應(yīng)該很快就會(huì)實(shí)現(xiàn)該方案,如果沒(méi)有拆分dex的項(xiàng)目的話,可以參考一下谷歌的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中的話,那么這個(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ì)讓改變的代碼生效。

最后編輯于
?著作權(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)容