JSPatch技術文檔

1、背景需求介紹

爲何咱們須要一個熱修復(hot-fix)技術?

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

2、JSPatch簡介

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

3、JSPatch與wax對比


JSPatch與Wax對比


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

4、JSPatch實現原理

基礎原理

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

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

也能夠替換某個類的方法爲新的實現:github

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,根據當前對象類型判斷進行不一樣操做:bash

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: 走原來的流程。

5、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); } } }, {})

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

1.版本更新策略

  • 考慮到下一個提交的App版本已經修復了上一個版本的bug,因此不一樣的App版本對應的補丁版本確定也不一樣。同一個App版本下,能夠出現遞增的補丁版本。
  • 補丁爲全量更新,即新版本補丁包括舊版補丁的內容,更新後新版補丁覆蓋舊版補丁。
  • 補丁分爲可選補丁和必選補丁,必選補丁用於重大bug的修復,若是不更新必選補丁則App沒法繼續使用。以下圖2中,補丁版本v1234對應各自版本的用戶,補丁v3爲必須更新,補丁v1,v2,v4爲可選補丁,則v1,v2的用戶必須更新到v4纔可以使用;而v3的用戶可先使用,同時後臺靜默更新到v4.

    股單App補丁版本更新策略

2.安全策略

安全問題在於JS 腳本可能被中間人攻擊替換代碼。可採起如下三種方法,股單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策略

7、參考資料和文獻:

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/



文/上官soyo(簡書做者) 原文連接:http://www.jianshu.com/p/0cb81bf23d7a 著做權歸做者全部,轉載請聯繫做者得到受權,並標註「簡書做者」。
相關文章
相關標籤/搜索