JSPatch技術文檔

一、背景需求介紹

為什么我們需要一個熱修復(hot-fix)技術?

  • 工作中容易犯錯、bug難以避免。
  • 開發和測試人力有限。
  • 蘋果Appstore審核周期太長,一旦出現嚴重bug難以快速上線新版本。
  • 作為生產力工具,用戶有對穩定性和可靠性的需求。

二、JSPatch簡介

JSPatch誕生于2015年5月,最初是騰訊廣研高級iOS開發@bang的個人項目。
它能夠使用JavaScript調用Objective-C的原生接口,從而動態植入代碼來替換舊代碼,以實現修復線上bug。
JSPatch在Github.com上開源后獲得了3000多個star和500多fork,廣受關注,目前已被應用在大量騰訊/阿里/百度的App中。

三、JSPatch與wax對比

JSPatch與Wax對比

最關鍵的是JSPatch可實現方法粒度的線上代碼替換,能修復一切代碼引起的Bug。
而Wax無法實現。

四、JSPatch實現原理

基礎原理

Objective-C是動態語言,具有運行時特性,該特性可通過類名稱和方法名的字符串獲取該類和該方法,并實例化和調用。

Class class = NSClassFromString(“UIViewController");
id viewController = [[class alloc] init];  
SEL selector = NSSelectorFromString(“viewDidLoad");
[viewController performSelector:selector];

也可以替換某個類的方法為新的實現:

static void newViewDidLoad(id slf, SEL sel) {}
class_replaceMethod(class, selector, newViewDidLoad, @"");

還可以新注冊一個類,為類添加方法:

Class cls = objc_allocateClassPair(superCls, "JPObject", 0);
objc_registerClassPair(cls);
class_addMethod(cls, selector, implement, typedesc);

Javascript調用

我們可以用Javascript對象定義一個Objective-C類:

{
  __isCls: 1,
  __clsName: "UIView"
}

在OC執行JS腳本前,通過正則把所有方法調用都改成調用 __c() 函數,再執行這個JS腳本,做到了類似OC/Lua/Ruby等的消息轉發機制:

UIView.alloc().init()
->
UIView.__c('alloc')().__c('init')()

給JS對象基類 Object 的 prototype 加上 __c 成員,這樣所有對象都可以調用到 __c,根據當前對象類型判斷進行不同操作:

Object.prototype.__c = function(methodName) {
  if (!this.__obj && !this.__clsName) return this[methodName].bind(this);
  var self = this
  return function(){
    var args = Array.prototype.slice.call(arguments)
    return _methodFunc(self.__obj, self.__clsName, methodName, args, self.__isSuper)
  }
}

互傳消息

JS和OC是通過JavaScriptCore互傳消息的。OC端在啟動JSPatch引擎時會創建一個 JSContext 實例,JSContext 是JS代碼的執行環境,可以給 JSContext 添加方法。JS通過調用 JSContext 定義的方法把數據傳給OC,OC通過返回值傳會給JS。調用這種方法,它的參數/返回值 JavaScriptCore 都會自動轉換,OC里的 NSArray, NSDictionary, NSString, NSNumber, NSBlock 會分別轉為JS端的數組/對象/字符串/數字/函數類型。
對于一個自定義id對象,JavaScriptCore 會把這個自定義對象的指針傳給JS,這個對象在JS無法使用,但在回傳給OC時OC可以找到這個對象。對于這個對象生命周期的管理,如果JS有變量引用時,這個OC對象引用計數就加1 ,JS變量的引用釋放了就減1,如果OC上沒別的持有者,這個OC對象的生命周期就跟著JS走了,會在JS進行垃圾回收時釋放。

方法替換

  1. 把UIViewController的 -viewWillAppear: 方法通過 class_replaceMethod() 接口指向 _objc_msgForward,這是一個全局 IMP,OC 調用方法不存在時都會轉發到這個 IMP 上,這里直接把方法替換成這個 IMP,這樣調用這個方法時就會走到 -forwardInvocation:。

  2. 為UIViewController添加 -ORIGviewWillAppear:-_JPviewWillAppear: 兩個方法,前者指向原來的IMP實現,后者是新的實現,稍后會在這個實現里回調JS函數。

  3. 改寫UIViewController的 -forwardInvocation: 方法為自定義實現。一旦OC里調用 UIViewController 的 -viewWillAppear: 方法,經過上面的處理會把這個調用轉發到 -forwardInvocation: ,這時已經組裝好了一個 NSInvocation,包含了這個調用的參數。在這里把參數從 NSInvocation 反解出來,帶著參數調用上述新增加的方法 -JPviewWillAppear: ,在這個新方法里取到參數傳給JS,調用JS的實現函數。整個調用過程就結束了,整個過程圖示如下:

JSPatch方法替換

最后一個問題,我們把 UIViewController 的 -forwardInvocation: 方法的實現給替換掉了,如果程序里真有用到這個方法對消息進行轉發,原來的邏輯怎么辦?首先我們在替換 -forwardInvocation: 方法前會新建一個方法 -ORIGforwardInvocation:,保存原來的實現IMP,在新的 -forwardInvocation: 實現里做了個判斷,如果轉發的方法是我們想改寫的,就走我們的邏輯,若不是,就調 -ORIGforwardInvocation: 走原來的流程。

五、JSPatch代碼示例

JSPatch在OC上的調用十分簡單

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 
[JPEngine startEngine]; 
NSString *sourcePath = [[NSBundle mainBundle] pathForResource:@"demo" ofType:@"js"]; 
NSString *script = [NSString stringWithContentsOfFile:sourcePath encoding:NSUTF8StringEncoding error:nil]; 
[JPEngine evaluateScript:script];
}

一個Javascript代碼修復Objective-C的bug的示例:


@implementation JPTableViewController

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
  NSString *content = self.dataSource[[indexPath row]];  //可能會超出數組范圍導致crash
  JPViewController *ctrl = [[JPViewController alloc] initWithContent:content];
  [self.navigationController pushViewController:ctrl];
}

@end

上述代碼中取數組元素處可能會超出數組范圍導致crash。如果在項目里引用了JSPatch,就可以下發JS腳本修復這個bug:

defineClass("JPTableViewController", {
  tableView_didSelectRowAtIndexPath: function(tableView, indexPath) {
    var row = indexPath.row()
    if (self.dataSource().length > row) {  //加上判斷越界的邏輯
      var content = self.dataArr()[row];
      var ctrl = JPViewController.alloc().initWithContent(content);
      self.navigationController().pushViewController(ctrl);
    }
  }
}, {})

六、股單App的Hot-fix解決方案

1.版本更新策略

  • 考慮到下一個提交的App版本已經修復了上一個版本的bug,所以不同的App版本對應的補丁版本肯定也不同。同一個App版本下,可以出現遞增的補丁版本。
  • 補丁為全量更新,即新版本補丁包括舊版補丁的內容,更新后新版補丁覆蓋舊版補丁。
  • 補丁分為可選補丁和必選補丁,必選補丁用于重大bug的修復,如果不更新必選補丁則App無法繼續使用。如下圖2中,補丁版本v1234對應各自版本的用戶,補丁v3為必須更新,補丁v1,v2,v4為可選補丁,則v1,v2的用戶必須更新到v4才可使用;而v3的用戶可先使用,同時后臺靜默更新到v4.


    股單App補丁版本更新策略

2.安全策略

安全問題在于JS 腳本可能被中間人攻擊替換代碼??刹扇∫韵氯N方法,股單App目前采用的是第三種:

1.對稱加密。如zip 的加密壓縮、AES 等加密算法。優點是簡單,缺點是安全性低,易破解。若客戶端被反編譯,密碼字段泄露,則完成破解。
2.HTTPS。優點是安全性高,證書在服務端未泄露,就不會被破解。缺點是部署麻煩,如果服務器本來就支持 HTTPS,使用這種方案也是一種不錯的選擇。
3.RSA校驗。安全性高,部署簡單。

RSA校驗

詳細校驗步驟如下:
1.服務端計算出腳本文件的 MD5 值,作為這個文件的數字簽名。
2.服務端通過私鑰加密第 1 步算出的 MD5 值,得到一個加密后的 MD5 值。
3.把腳本文件和加密后的 MD5 值一起下發給客戶端。
4.客戶端拿到加密后的 MD5 值,通過保存在客戶端的公鑰解密。
5.客戶端計算腳本文件的 MD5 值。
6.對比第 4/5 步的兩個 MD5 值(分別是客戶端和服務端計算出來的 MD5 值),若相等則通過校驗。

3.客戶端策略

客戶端具體策略如下圖:
1.用戶打開App時,同步進行本地補丁的加載。
2.用戶打開App時,后臺進程發起異步網絡請求,獲取服務器中當前App版本所對應的最新補丁版本和必須的補丁版本。
3.獲取補丁版本的請求回來后,跟本地的補丁版本進行對比。
4.如果本地補丁版本小于必須版本,則提示用戶,展示下載補丁界面,進行進程同步的補丁下載。下載完成后重新加載App和最新補丁,再進入App。
5.如果本地補丁版本不小于必須版本,但小于最新版本,則進入App,不影響用戶操作。同時進行后臺進程異步靜默下載,下載后補丁保存在本地。下次App啟動時再加載最新補丁。
6.如果版本為最新,則進入App。

股單App客戶端hot-fix策略

七、參考資料和文獻:

1.https://github.com/bang590/JSPatch
2.https://github.com/mmin18/WaxPatch
3.https://github.com/probablycorey/wax
4.https://github.com/alibaba/AndFix
5.http://blog.cnbang.net/tech/2879/
6.http://blog.cnbang.net/works/2767/
7.http://blog.cnbang.net/tech/2808/
8.http://blog.zhaiyifan.cn/2015/11/20/HotPatchCompare/

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,048評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,414評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,169評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,722評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,465評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,823評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,813評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,000評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,554評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,295評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,513評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,035評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,722評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,125評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,430評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,237評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,482評論 2 379

推薦閱讀更多精彩內容