iOS【JSPatch熱更新】實踐一

JSPatch 是一個開源項目(Github鏈接),只需要在項目里引入極小的引擎文件,
使用JavaScript調用任何Objective-C的原生接口,替換任意 Objective-C 原生方法。
目前主要用于下發 JS 腳本替換原生 Objective-C 代碼,實時修復線上 bug。

例如線上 APP 有一段代碼出現 bug 導致 crash:

//OC FFEasyLifeHomeCtrl
self.nameArrays = @[@"0",@"1",@"2",@"3",@"4"];
...
- (void)testArray {//測試數組越界
    NSString *testName = self.nameArrays[5];
}

//JS
defineClass('FFEasyLifeHomeCtrl',['data','nameArray','totalCount'], {
        testArray: function() {//修改數組越界
            var testName = self.nameArrays([4]);
       }
});

除了修復 bug,JSPatch也可以用于動態運營,實時修改線上APP行為,
或動態添加功能。JSPatch 詳細使用文檔見 [Github Wiki](https://github.com/bang590/JSPatch/wiki)。

JSPatch優勢:

1、JSPatch更符合Apple的規則。iOS Developer Program License Agreement里3.3.2提到不可動態下發可執行代碼,但通過蘋果JavaScriptCore.framework或WebKit執行的代碼除外,JS正是通過JavaScriptCore.framework執行的

2、使用系統內置的JavaScriptCore.framework,無需內嵌腳本引擎,體積小巧

3、支持block

JSPatch缺點:

1、JSPatch劣勢在于不支持iOS6,因為需要引入JavaScriptCore.framework

2、持續改進中存在風險:JSPatch讓腳本語言獲得調用所有原生OC方法的能力,不像web前端把能力局限在瀏覽器,使用上會有一些安全風險

3、若在網絡傳輸過程中下發明文JS,可能會被中間人篡改JS腳本,執行任意方法,盜取APP里的相關信息,危及用戶信息和APP

4、若下載完后的JS保存在本地沒有加密,在越獄的機器上用戶也可以手動替換或篡改腳本

JSPatch 風險

1、JSPatch腳本的執行權限很高,若在傳輸過程中被中間人篡改,會帶來很大的安全問題,為了防止這種情況出現,在傳輸過程中對JS文件進行了RSA簽名加密,流程如下:
服務端:計算JS文件MD5值。用RSA私鑰對MD5值進行加密,與JS文件一起下發給客戶端。
客戶端:拿到加密數據,用RSA公鑰解密出MD5值。本地計算返回的JS文件MD5值。對比上述的兩個MD5值,若相等則校驗通過,取JS文件保存到本地。
由于RSA是非對稱加密,在沒有私鑰的情況下第三方無法加密對應的MD5值,也就無法偽造JS文件,杜絕了JS文件在傳輸過程被篡改的可能。

2、本地存儲
本地存儲的腳本被篡改的機會小很多,只在越獄機器上有點風險,對此JSPatch SDK在下載完腳本保存到本地時也進行了簡單的對稱加密,每次讀取時解密。

更新能力

React Native 和 JSPatch 都能對用其開發出來的功能模塊進行熱更新,這也是這種方案最大的好處。

React Native: 在熱更新時無法使用事先沒有做過橋接的原生組件,例如需要加一個發送短信功能,需要用到原生 MessageUI.framework 的接口,若沒有在編譯時加上提供給 JavaScript 的接口,是無法調用到的。

JSPatch: 可以調用到任意已在項目里的組件,以及任意原生 framework 接口,不需要事先做橋接,在熱更新的能力上,相對來說 JSPatch 的能力和自由度會更高一些。

性能體驗

JSPatch 的性能問題主要在于 JavaScript 和 Objective-C 的通信,每次調用 Objective-C 方法都要通過 Objective-C Runtime 接口,并進行參數轉換。
runtime 接口調用帶來的耗時一般不會成為瓶頸,參數轉換則需要注意避免在 JavaScript 和 Objective-C 之間傳遞大的數據集合對象。
JSPatch 在性能方面也針對開發功能做了不少優化,盡力減少了 JavaScript 和 Objective-C 的通信,來看并沒有碰到太多性能問題。

集成JSPatch過程——>SDK接入

第一步 獲得 AppKey

在平臺上注冊帳號,可以任意添加新 App,每一個 App都有一個唯一的 AppKey 作為標識。
網站:http://jspatch.com/Apps

第二步 集成SDK

通過 cocoapods 集成

在 podfile 中添加命令:
pod 'JSPatchPlatform'
再執行 pod install 即可。

手動集成
若沒有使用 cocoapods,也可以手動集成。下載 SDK 后解壓,將 JSPatchPlatform.framework 拖入項目中,
勾選 "Copy items if needed",并確保 "Add to target" 勾選了相應的 target。

添加依賴框架:TARGETS -> Build Phases -> Link Binary With Libraries -> + 添加 libz.dylib 和 JavaScriptCore.framework。

注意:手動集成無法斷點調試 JSPatch 核心源碼,推薦使用 cocoapods 方式集成。

第三步 運行
在 AppDelegate.m 里載入文件,并調用 +startWithAppKey: 方法,參數為第一步獲得的 AppKey。接著調用 +sync 方法檢查更新。

例子:
#import <JSPatchPlatform/JSPatch.h>

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [JSPatch startWithAppKey:@"你的AppKey"];
    [JSPatch sync];
    ...
}
@end
至此 JSPatch 接入完畢,下一步可以開始在后臺為這個 App 添加 JS 補丁文件了。

上述例子是把 JSPatch 同步放在 -application:didFinishLaunchingWithOptions: 里,
若希望補丁能及時推送,可以把 [JSPatch sync] 放在 -applicationDidBecomeActive: 里,每次喚醒都能同步更新 JSPatch 補丁,不需要等用戶下次啟動。

項目結構

  • 圖片 1.png

本地創建main.js
終端創建JS文件命令:touch test.js

項目代碼

#import "AppDelegate.h"
#import <JSPatchPlatform/JSPatch.h>

@interface AppDelegate ()
@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    //[self hotJSPatch];
    //本地測試
   [self hotLocalJSPatch];

    return YES;
}

- (void)hotJSPatch {

    //傳入在平臺申請的 appKey。會自動執行已下載到本地的 patch 腳本。
    [JSPatch startWithAppKey:@"c38c725a42102b45"];


    /*
     定義用戶屬性
     用于條件下發,例如:
     [JSPatch setupUserData:@{@"userId": @"100867", @"location": @"guangdong"}];
     在 `+sync:` 之前調用
     */
    //[JSPatch setupUserData:@{@"userId": @"1000876", @"isMale": @(1)}];

    #ifdef DEBUG
    //進入開發模式
    [JSPatch setupDevelopment];  
    #endif

     //與 JSPatch 平臺后臺同步,發請求詢問后臺是否有 patch 更新,如果有更新會自動下載并執行可調用多次(App啟動時調用或App喚醒時調)
    [JSPatch sync];

    //在狀態欄顯示調試按鈕,點擊可以看到所有 JSPatch 相關的 log 和內容

    [JSPatch showDebugView];
}

- (void) hotLocalJSPatch {

    //用于發布前測試腳本
    //測試完成后請刪除,改為調用 +startWithAppKey: 和 +sync

    //加載本地js調試
    [JSPatch testScriptInBundle];

    //在狀態欄顯示調試按鈕,點擊可以看到所有 JSPatch 相關的 log 和內容
    [JSPatch showDebugView];

}
FFEasyLifeHomeCtrl.m
====================
1. 數組越界;
2. 未實現按鈕事件方法
====================

#import "FFEasyLifeHomeCtrl.h"

@interface FFEasyLifeHomeCtrl ()
@property (nonatomic, strong) NSArray      *nameArrays;
@property (nonatomic, strong) UIButton     *catButton;

@end

@implementation FFEasyLifeHomeCtrl
- (void)viewDidLoad {
    [super viewDidLoad];

    [self.view addSubview:self.catButton];
    [self testArray];

}

// MARK: - 方法
- (void)testArray {//數組越界
    NSString *testName = self.nameArrays[5];
}

// MARK: - getter
- (UIButton *)catButton {
    if (!_catButton) {
        _catButton = [UIButton buttonWithType:UIButtonTypeCustom];
        _catButton.backgroundColor = k_COLORRANDOM;
        [_catButton setTitle:@"美圖" forState:UIControlStateNormal];

        /** 未實現事件方法 */
        [_catButton addTarget:self action:@selector(actionPicture:) forControlEvents:UIControlEventTouchUpInside];
   }
    return _catButton;
}

JS代碼

main.js
====================
1.處理數組越界問題;
2.添加按鈕事件方法;
3.跳轉到一個新控制器(用js創建的新控制器)
====================

include(‘FFEasyLifeHomeCtrl.js')

//用js創建的新控制器
include('FFEasyLifePictureCtrl.js')
FFEasyLifeHomeCtrl.js
require('UIView, UIColor, UILabel, UIFont, UIImageView, UIImage')
require('FFEasyLifePictureCtrl')

defineClass('FFEasyLifeHomeCtrl', {
        
     testArray: function() {
           // 1. 處理數組越界問題
           var nameArrays = self.nameArrays().toJS();
           var testName = nameArrays[4];
           console.log('----- testName: ' + testName);       
     },

     // 2. 添加按鈕事件方法
     actionPicture: function(button) {
          var ctrl = FFEasyLifePictureCtrl.alloc().init();
          ctrl.view().setBackgroundColor(UIColor.lightGrayColor());
          //self.navigationController().pushViewController_animated(ctrl, YES);
          
          3. 跳轉到一個新控制器(用js創建的新控制器)
          self.presentViewController_animated_completion(ctrl, YES, null);
     },
});
FFEasyLifePictureCtrl.js
require('UIColor');

defineClass('FFEasyLifePictureCtrl : UITableViewController <UIAlertViewDelegate>', ['data'], {
        
        init: function() {
            self = self.super().init()
            return self
        },
        
        viewDidLoad: function() {
        },
        
        dataSource: function() {
        
            //數組
            var data = self.data();
            if (data) return data;
        
            var data = [];
            for (var i = 0; i < 20; i ++) {
                data.push("cell from js " + i);
            }
        
            self.setData(data)
            console.log('data:'  + self.data());

            return data;
        },
        
        // MARK: - tableDelegate
        numberOfSectionsInTableView: function(tableView) {
            return 1;
        },
        
        tableView_numberOfRowsInSection: function(tableView, section) {
            return self.dataSource().length;
        },
        
        tableView_heightForRowAtIndexPath: function(tableView, indexPath) {
            return 200;
        },
        
        tableView_cellForRowAtIndexPath: function(tableView, indexPath) {
            var cell = tableView.dequeueReusableCellWithIdentifier("cell")
            if (!cell) {
                cell = require('UITableViewCell').alloc().initWithStyle_reuseIdentifier(0, "cell")
            }
            cell.textLabel().setText(self.dataSource()[indexPath.row()])
            cell.setBackgroundColor(UIColor.colorWithRed_green_blue_alpha((Math.random() *255) / 255.0, (Math.random() *255) / 255.0, (Math.random() *255) / 255.0, 1));

            return cell
        },
        
        tableView_didSelectRowAtIndexPath: function(tableView, indexPath) {
        
            //彈窗
            var alertView = require('UIAlertView').alloc().initWithTitle_message_delegate_cancelButtonTitle_otherButtonTitles("Alert",self.dataSource()[indexPath.row()], self, "OK",  null);
            alertView.show()
        },
        
        alertView_willDismissWithButtonIndex: function(alertView, idx) {
            console.log('click btn ' + alertView.buttonTitleAtIndex(idx).toJS())
        }
})

JSPatch 創建應用

  • app.png

JSPatch執行順序問題:

JSPatch所有動態替換的函數,都必須在JS執行完了之后,第二次再執行,才會全面以新替換的js代碼進行工作。
時間順序
#? application:didFinishLaunchingWithOptions:
#? JSPatch發起網絡請求拉patch
#? app的rootViewController觸發ViewDidload運行完畢,依然是未修正的錯誤界面
#? JSPatch網絡請求拉取回來,執行JS
#? JS已經執行成功ViewDidLoad已經被替換,但是界面已經生成,新的正確的ViewDidLoad并不會再次執行
效果:我的viewDidLoad為啥不能修改啊?
比喻:
#? viewDidload的函數代碼就好比建筑設計圖
#? 運行起來后的界面就好比建好的建筑
時間順序:
#? viewDidLoad有bug需要改(建筑設計圖圖紙錯了)
#? 舊viewDidLoad先執行,并且創建好了界面(工人已經按著錯圖紙把建筑建好了)
#? JSPatch執行了hotfix(設計師修改設計圖紙)
#? JSPatch看起來沒效果(就算你改好了建筑圖紙,已經建好的建筑是不會有任何改變的)

解決辦法:2個(未去實踐過)
#? 在建造建筑之前,把圖紙改好
JSPatch在使用的時候,第一次下載網絡請求是要時間的,所以才會發生修改圖紙,在建筑建好之后。
但是補丁已經下載完成,第二次運行app,新的圖紙已經存在本地,是可以在創建rootViewController之前,就先把patch運行,讓新圖紙生效的。

#?  不要修改圖紙了,直接去修改建筑
當你網絡請求在JSPatch下載完Patch之后,通過callback,進行完全自定義的處理,窗戶壞了,直接改窗戶,門壞了修門,你也可以自定義把房子推倒了重建
如果你使用的是JSPatchSDK,那么頭文件有一個callback的API,JSPatchSDK提供了JS下載完成的這個時機,具體怎么修,純看使用者自己

幫助網址:
JSPatch官網:http://jspatch.com
JSPatch官方文檔:http://jspatch.com/Docs/dev

注意項:
1:補丁版本號與app版本號一樣;
2:多個js時,放在一個文件夾里壓縮成zip;

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

推薦閱讀更多精彩內容

  • 簡介: JSPatch是最近業余做的項目,只需在項目中引入極小的引擎,就可以使用JavaScript調用任何...
    Luciena閱讀 951評論 0 8
  • 1, JSPatch熱更新 眾所周知,AppStore 上發布需要有個一非常惡心的審核期,而且很可能被拒絕掉,發布...
    嘹亮的浩哥閱讀 1,089評論 2 3
  • JSPatch是什么 JSPatch是一個開源項目,只需要在項目里引入極小的引擎文件,就可以使用 JavaScri...
    ImmortalSummer閱讀 2,552評論 7 11
  • 背景介紹 IOS平臺提交審核的周期太長,快則45天,慢則半個月或者20天,如果碰到圣誕節等假日,可能一個月都有可能...
    恒源賓館閱讀 2,268評論 10 27
  • 和朋友吃飯,餐剛到,來了一個大叔,高高的個子,帶著一個鼓鼓的舊帆布包。周圍有很多干凈的桌子,他偏偏走向角落未收拾的...
    阿北先生閱讀 241評論 0 2