原文鏈接:公眾號:QQ空間終端開發(fā)團隊
安卓App熱補丁動態(tài)修復(fù)技術(shù)介紹
TPatch是一套使用JavaScript給iOS打熱補丁的系統(tǒng),能非常有效的解決線上App的Crash和各種問題超全。
1.從何而來?
對于每一個開發(fā),從寫Hello World開始邓馒,到使用各種語言嘶朱,可能都會遇到各種BUG。有的BUG能快速解決光酣,比如Web側(cè)的疏遏,發(fā)個JS或者Html即可。但是在終端開發(fā)中救军,比如iOS财异,發(fā)現(xiàn)的線上問題往往沒那么快能解決,換包可能需要Apple短則幾天長則一周以上的審核唱遭,成本很高戳寸。有沒有辦法能快速解決iOS App的線上問題?TPatch是其中一種比較好的解決方案拷泽。
2.TPatch特點
支持多線程:
使用JS打補丁的天然優(yōu)勢在于JavaScriptCore是線程安全的疫鹊,雖然鎖的粒度有點大,并且有些方法的鎖有問題(這些在TPatch都已解決)司致。
支持Block:
JS中的function和OC的Block有很多相似之處拆吆。有補丁中定義的function,傳遞到OC脂矫,我們會轉(zhuǎn)成Block枣耀,并且Block可以在OC和JS之間傳遞,這點Lua補丁是很難做到庭再。
異步機制:
由于JavsSciptCore是線程安全的捞奕,同時也帶來另外一個問題牺堰,假如工作線程和主線程都打了補丁,工作線程的補丁耗時非常嚴重颅围,這時候如果主線程補丁開始運行萌焰,就會被阻塞。TPatch引入了異步機制谷浅,能讓進入JSCore的補丁快速返回,異步執(zhí)行奶卓,減少補丁之間的影響一疯。
支持在線Reset回滾:
在補丁發(fā)布后,有可能通過監(jiān)控發(fā)現(xiàn)補丁有問題夺姑,這時候用戶側(cè)的運行邏輯已經(jīng)被“污染”墩邀。TPatch支持,在補丁后臺設(shè)置該補丁過期后盏浙,用戶側(cè)App會刪掉本地有問題的補丁包眉睹,并且在線Reset,而不是等App重啟后再恢復(fù)废膘,下次重啟可能得好幾天竹海。
調(diào)試方便:
利用JavaScriptCore的天然優(yōu)勢,其內(nèi)部提供了Debug接口丐黄。我們可以像調(diào)試App里面的網(wǎng)頁一樣斋配,使用Mac下的Safari遠程調(diào)試補丁,斷點灌闺、堆棧艰争、異常等一目了然。
精準投放:
TPatch支持按用戶桂对、iOS版本甩卓、業(yè)務(wù)App版本和Mask標記投放。Mask是一個可擴展的bit標記蕉斜,業(yè)務(wù)可以自定義逾柿,比如取一位越獄標記,或者網(wǎng)絡(luò)標記蛛勉,補丁就可以根據(jù)是否越獄和網(wǎng)絡(luò)標記下發(fā)鹿寻。
3.核心原理
TPatch包括補丁包后臺系統(tǒng)和終端組件,其核心原理是補丁后臺根據(jù)補丁配置诽凌,下發(fā)一段補丁JS給終端毡熏,終端執(zhí)行這段補丁,利用OC Runtime覆蓋有問題的方法或者執(zhí)行一段邏輯侣诵,修正運行時的邏輯痢法,從而達到修復(fù)BUG的目的狱窘。
4.打補丁流程
1.在補丁后臺下發(fā)補丁腳本后,首先會經(jīng)過iOS7及以上系統(tǒng)自帶的JavaScriptCore.framework把JS補丁執(zhí)行起來财搁,通過調(diào)用TPatch.js里面的Bridge接口蘸炸,調(diào)用到OC里面打補丁的方法,打上補丁尖奔。
2.當業(yè)務(wù)代碼執(zhí)行這段已經(jīng)打了補丁的功能時搭儒,不會是原來的OC代碼,而是一段JS代碼提茁。JS可以通過JS引起和OC引擎支持Block淹禾、異步執(zhí)行等,并且支持在線Reset回滾茴扁。
5.和其他方案對比
waxPatch:
是使用Lua+Wax打補丁的方案铃岔,App需要集成Lua解釋器和Wax框架(接近1M)。不過waxPatch對Block不太完善峭火,多線程補丁也可能有問題毁习,Wax也已經(jīng)兩年沒人維護。相比之下TPatch更加輕量卖丸,對安裝包影響僅200K纺且,功能也更加強大。
JSPatch:
同樣使用JS來打補丁坯苹,和TPatch終端組件核心原理是相似的隆檀。不過JSPatch在實際的海量產(chǎn)品中運用還有不少問題沒解決,比如Block傳遞粹湃、多線程Crash等問題恐仑,TPatch解決了這些問題,更加穩(wěn)定为鳄,并且支持異步機制裳仆、動態(tài)回滾等優(yōu)化特性。
1.背景
當一個App發(fā)布之后孤钦,突然發(fā)現(xiàn)了一個嚴重bug需要進行緊急修復(fù)歧斟,這時候公司各方就會忙得焦頭爛額:重新打包App、測試偏形、向各個應(yīng)用市場和渠道換包静袖、提示用戶升級、用戶下載俊扭、覆蓋安裝队橙。有時候僅僅是為了修改了一行代碼,也要付出巨大的成本進行換包和重新發(fā)布。
這時候就提出一個問題:有沒有辦法以補丁的方式動態(tài)修復(fù)緊急Bug捐康,不再需要重新發(fā)布App仇矾,不再需要用戶重新下載,覆蓋安裝解总?
雖然Android系統(tǒng)并沒有提供這個技術(shù)贮匕,但是很幸運的告訴大家,答案是:可以花枫,我們QQ空間提出了熱補丁動態(tài)修復(fù)技術(shù)來解決以上這些問題刻盐。
2.實際案例
空間Android獨立版5.2發(fā)布后,收到用戶反饋劳翰,結(jié)合版無法跳轉(zhuǎn)到獨立版的訪客界面隙疚,每天都較大的反饋。在以前只能緊急換包磕道,重新發(fā)布。成本非常高行冰,也影響用戶的口碑溺蕉。最終決定使用熱補丁動態(tài)修復(fù)技術(shù),向用戶下發(fā)Patch悼做,在用戶無感知的情況下疯特,修復(fù)了外網(wǎng)問題,取得非常好的效果肛走。
3.解決方案
該方案基于的是android dex分包方案的漓雅,關(guān)于dex分包方案,網(wǎng)上有幾篇解釋了朽色,所以這里就不再贅述邻吞,具體可以看這里https://m.oschina.net/blog/308583。
簡單的概括一下葫男,就是把多個dex文件塞入到app的classloader之中抱冷,但是android dex拆包方案中的類是沒有重復(fù)的,如果classes.dex和classes1.dex中有重復(fù)的類梢褐,當用到這個重復(fù)的類的時候旺遮,系統(tǒng)會選擇哪個類進行加載呢?
讓我們來看看類加載的代碼:
一個ClassLoader可以包含多個dex文件盈咳,每個dex文件是一個Element耿眉,多個dex文件排列成一個有序的數(shù)組dexElements,當找類的時候鱼响,會按順序遍歷dex文件鸣剪,然后從當前遍歷的dex文件中找類,如果找類則返回,如果找不到從下一個dex文件繼續(xù)查找西傀。
理論上斤寇,如果在不同的dex中有相同的類存在,那么會優(yōu)先選擇排在前面的dex文件的類拥褂,如下圖:
在此基礎(chǔ)上娘锁,我們構(gòu)想了熱補丁的方案,把有問題的類打包到一個dex(patch.dex)中去饺鹃,然后把這個dex插入到Elements的最前面莫秆,如下圖:
好,該方案基于第二個拆分dex的方案悔详,方案實現(xiàn)如果懂拆分dex的原理的話镊屎,大家應(yīng)該很快就會實現(xiàn)該方案,如果沒有拆分dex的項目的話茄螃,可以參考一下谷歌的multidex方案實現(xiàn)缝驳。然后在插入數(shù)組的時候,把補丁包插入到最前面去归苍。
好用狱,看似問題很簡單,輕松的搞定了拼弃,讓我們來試驗一下夏伊,修改某個類,然后打包成dex吻氧,插入到classloader溺忧,當加載類的時候出現(xiàn)了(本例中是QzoneActivityManager要被替換):
為什么會出現(xiàn)以上問題呢?
從log的意思上來講盯孙,ModuleManager引用了QzoneActivityManager鲁森,但是發(fā)現(xiàn)這這兩個類所在的dex不在一起,其中:
1. ModuleManager在classes.dex中
2. QzoneActivityManager在patch.dex中
結(jié)果發(fā)生了錯誤振惰。
這里有個問題,拆分dex的很多類都不是在同一個dex內(nèi)的,怎么沒有問題?
讓我們搜索一下拋出錯誤的代碼所在刀森,嘿咻嘿咻,找到了一下代碼:
從代碼上來看报账,如果兩個相關(guān)聯(lián)的類在不同的dex中就會報錯研底,但是拆分dex沒有報錯這是為什么,原來這個校驗的前提是:
如果引用者(也就是ModuleManager)這個類被打上了CLASS_ISPREVERIFIED標志透罢,那么就會進行dex的校驗榜晦。那么這個標志是什么時候被打上去的?讓我們在繼續(xù)搜索一下代碼羽圃,嘿咻嘿咻~~乾胶,在DexPrepare.cpp找到了一下代碼:
這段代碼是dex轉(zhuǎn)化成odex(dexopt)的代碼中的一段,我們知道當一個apk在安裝的時候,apk中的classes.dex會被虛擬機(dexopt)優(yōu)化成odex文件识窿,然后才會拿去執(zhí)行斩郎。
虛擬機在啟動的時候,會有許多的啟動參數(shù)喻频,其中一項就是verify選項缩宜,當verify選項被打開的時候,上面doVerify變量為true甥温,那么就會執(zhí)行dvmVerifyClass進行類的校驗锻煌,如果dvmVerifyClass校驗類成功,那么這個類會被打上CLASS_ISPREVERIFIED的標志姻蚓,那么具體的校驗過程是什么樣子的呢宋梧?
此代碼在DexVerify.cpp中,如下:
1. 驗證clazz->directMethods方法狰挡,directMethods包含了以下方法:
1. static方法
2. private方法
3. 構(gòu)造函數(shù)
2.clazz->virtualMethods
1. 虛函數(shù)=override方法?
概括一下就是如果以上方法中直接引用到的類(第一層級關(guān)系捂龄,不會進行遞歸搜索)和clazz都在同一個dex中的話,那么這個類就會被打上CLASS_ISPREVERIFIED:
所以為了實現(xiàn)補丁方案加叁,所以必須從這些方法中入手跺讯,防止類被打上CLASS_ISPREVERIFIED標志。
最終空間的方案是往所有類的構(gòu)造函數(shù)里面插入了一段代碼殉农,代碼如下:
if (ClassVerifier.PREVENT_VERIFY) {
System.out.println(AntilazyLoad.class);
}
其中AntilazyLoad類會被打包成單獨的hack.dex,這樣當安裝apk的時候局荚,classes.dex內(nèi)的類都會引用一個在不相同dex中的AntilazyLoad類超凳,這樣就防止了類被打上CLASS_ISPREVERIFIED的標志了,只要沒被打上這個標志的類都可以進行打補丁操作耀态。
然后在應(yīng)用啟動的時候加載進來.AntilazyLoad類所在的dex包必須被先加載進來,不然AntilazyLoad類會被標記為不存在轮傍,即使后續(xù)加載了hack.dex包,那么他也是不存在的首装,這樣屏幕就會出現(xiàn)茫茫多的類AntilazyLoad找不到的log创夜。
所以Application作為應(yīng)用的入口不能插入這段代碼。(因為載入hack.dex的代碼是在Application中onCreate中執(zhí)行的仙逻,如果在Application的構(gòu)造函數(shù)里面插入了這段代碼驰吓,那么就是在hack.dex加載之前就使用該類,該類一次找不到系奉,會被永遠的打上找不到的標志)
其中:
之所以選擇構(gòu)造函數(shù)是因為他不增加方法數(shù)檬贰,一個類即使沒有顯式的構(gòu)造函數(shù),也會有一個隱式的默認構(gòu)造函數(shù)缺亮。
空間使用的是在字節(jié)碼插入代碼,而不是源代碼插入翁涤,使用的是javaassist庫來進行字節(jié)碼插入的。
隱患:
虛擬機在安裝期間為類打上CLASS_ISPREVERIFIED標志是為了提高性能的,我們強制防止類被打上標志是否會影響性能葵礼?這里我們會做一下更加詳細的性能測試.但是在大項目中拆分dex的問題已經(jīng)比較嚴重号阿,很多類都沒有被打上這個標志。
如何打包補丁包:
1. 空間在正式版本發(fā)布的時候鸳粉,會生成一份緩存文件扔涧,里面記錄了所有class文件的md5,還有一份mapping混淆文件赁严。
2. 在后續(xù)的版本中使用-applymapping選項扰柠,應(yīng)用正式版本的mapping文件,然后計算編譯完成后的class文件的md5和正式版本進行比較疼约,把不相同的class文件打包成補丁包卤档。
備注:該方案現(xiàn)在也應(yīng)用到我們的編譯過程當中,編譯不需要重新打包dex,只需要把修改過的類的class文件打包成patch dex,然后放到sdcard下,那么就會讓改變的代碼生效。