iOS【JSPatch熱更新】實踐一

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

例如線上 APP 有一段代碼出現(xiàn) bug 導致 crash:

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

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

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

JSPatch優(yōu)勢:

1、JSPatch更符合Apple的規(guī)則袒啼。iOS Developer Program License Agreement里3.3.2提到不可動態(tài)下發(fā)可執(zhí)行代碼哈扮,但通過蘋果JavaScriptCore.framework或WebKit執(zhí)行的代碼除外,JS正是通過JavaScriptCore.framework執(zhí)行的

2蚓再、使用系統(tǒng)內置的JavaScriptCore.framework滑肉,無需內嵌腳本引擎,體積小巧

3摘仅、支持block

JSPatch缺點:

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

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

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

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

JSPatch 風險

1洞就、JSPatch腳本的執(zhí)行權限很高悼粮,若在傳輸過程中被中間人篡改,會帶來很大的安全問題砚亭,為了防止這種情況出現(xiàn)灯变,在傳輸過程中對JS文件進行了RSA簽名加密,流程如下:
服務端:計算JS文件MD5值捅膘。用RSA私鑰對MD5值進行加密添祸,與JS文件一起下發(fā)給客戶端。
客戶端:拿到加密數(shù)據(jù)寻仗,用RSA公鑰解密出MD5值刃泌。本地計算返回的JS文件MD5值。對比上述的兩個MD5值署尤,若相等則校驗通過耙替,取JS文件保存到本地。
由于RSA是非對稱加密沐寺,在沒有私鑰的情況下第三方無法加密對應的MD5值林艘,也就無法偽造JS文件,杜絕了JS文件在傳輸過程被篡改的可能。

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

更新能力

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

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

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

性能體驗

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

集成JSPatch過程——>SDK接入

第一步 獲得 AppKey

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

第二步 集成SDK

通過 cocoapods 集成

在 podfile 中添加命令:
pod 'JSPatchPlatform'
再執(zhí)行 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: 方法炼列,參數(shù)為第一步獲得的 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

本地創(chuàng)建main.js
終端創(chuàng)建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熊赖。會自動執(zhí)行已下載到本地的 patch 腳本。
    [JSPatch startWithAppKey:@"c38c725a42102b45"];


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

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

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

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

    [JSPatch showDebugView];
}

- (void) hotLocalJSPatch {

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

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

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

}
FFEasyLifeHomeCtrl.m
====================
1. 數(shù)組越界娇未;
2. 未實現(xiàn)按鈕事件方法
====================

#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 {//數(shù)組越界
    NSString *testName = self.nameArrays[5];
}

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

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

JS代碼

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

include(‘FFEasyLifeHomeCtrl.js')

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

defineClass('FFEasyLifeHomeCtrl', {
        
     testArray: function() {
           // 1. 處理數(shù)組越界問題
           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創(chuàng)建的新控制器)
          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() {
        
            //數(shù)組
            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 創(chuàng)建應用

  • app.png

JSPatch執(zhí)行順序問題:

JSPatch所有動態(tài)替換的函數(shù),都必須在JS執(zhí)行完了之后镊讼,第二次再執(zhí)行宽涌,才會全面以新替換的js代碼進行工作。
時間順序
#? application:didFinishLaunchingWithOptions:
#? JSPatch發(fā)起網(wǎng)絡請求拉patch
#? app的rootViewController觸發(fā)ViewDidload運行完畢蝶棋,依然是未修正的錯誤界面
#? JSPatch網(wǎng)絡請求拉取回來卸亮,執(zhí)行JS
#? JS已經(jīng)執(zhí)行成功ViewDidLoad已經(jīng)被替換,但是界面已經(jīng)生成玩裙,新的正確的ViewDidLoad并不會再次執(zhí)行
效果:我的viewDidLoad為啥不能修改凹婷场?
比喻:
#? viewDidload的函數(shù)代碼就好比建筑設計圖
#? 運行起來后的界面就好比建好的建筑
時間順序:
#? viewDidLoad有bug需要改(建筑設計圖圖紙錯了)
#? 舊viewDidLoad先執(zhí)行吃溅,并且創(chuàng)建好了界面(工人已經(jīng)按著錯圖紙把建筑建好了)
#? JSPatch執(zhí)行了hotfix(設計師修改設計圖紙)
#? JSPatch看起來沒效果(就算你改好了建筑圖紙溶诞,已經(jīng)建好的建筑是不會有任何改變的)

解決辦法:2個(未去實踐過)
#? 在建造建筑之前,把圖紙改好
JSPatch在使用的時候决侈,第一次下載網(wǎng)絡請求是要時間的螺垢,所以才會發(fā)生修改圖紙,在建筑建好之后赖歌。
但是補丁已經(jīng)下載完成枉圃,第二次運行app,新的圖紙已經(jīng)存在本地庐冯,是可以在創(chuàng)建rootViewController之前孽亲,就先把patch運行,讓新圖紙生效的展父。

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

幫助網(wǎng)址:
JSPatch官網(wǎng):http://jspatch.com
JSPatch官方文檔:http://jspatch.com/Docs/dev

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市坡贺,隨后出現(xiàn)的幾起案子官辈,更是在濱河造成了極大的恐慌箱舞,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件拳亿,死亡現(xiàn)場離奇詭異晴股,居然都是意外死亡,警方通過查閱死者的電腦和手機肺魁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門电湘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人鹅经,你說我怎么就攤上這事寂呛。” “怎么了瘾晃?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵贷痪,是天一觀的道長。 經(jīng)常有香客問我蹦误,道長呢诬,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任胖缤,我火速辦了婚禮,結果婚禮上阀圾,老公的妹妹穿的比我還像新娘哪廓。我一直安慰自己,他們只是感情好初烘,可當我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布涡真。 她就那樣靜靜地躺著,像睡著了一般肾筐。 火紅的嫁衣襯著肌膚如雪哆料。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天吗铐,我揣著相機與錄音东亦,去河邊找鬼。 笑死唬渗,一個胖子當著我的面吹牛典阵,可吹牛的內容都是我干的。 我是一名探鬼主播镊逝,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼壮啊,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了撑蒜?” 一聲冷哼從身側響起歹啼,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤玄渗,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后狸眼,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體藤树,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年份企,在試婚紗的時候發(fā)現(xiàn)自己被綠了也榄。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡司志,死狀恐怖甜紫,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情骂远,我是刑警寧澤囚霸,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站激才,受9級特大地震影響拓型,放射性物質發(fā)生泄漏。R本人自食惡果不足惜瘸恼,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一劣挫、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧东帅,春花似錦压固、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至愧膀,卻和暖如春拦键,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背檩淋。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工芬为, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蟀悦。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓碳柱,卻偏偏與公主長得像,于是被迫代替她去往敵國和親熬芜。 傳聞我的和親對象是個殘疾皇子莲镣,可洞房花燭夜當晚...
    茶點故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內容

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