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 补丁,不需要等用户下次启动。
项目结构
本地创建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 创建应用
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;