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;