JSPatch是什么
JSPatch是一个开源项目,只需要在项目里引入极小的引擎文件,就可以使用 JavaScript 调用任何 Objective-C 的原生接口,替换任意 Objective-C 原生方法。目前主要用于下发 JS 脚本替换原生 Objective-C 代码,实时修复线上 bug.
目录
- JSPatch接入
- 示例代码
- 安全问题
- 自定义RSA Key
- 常见问题:发布补丁后不起作用
- 一点儿小总结
JSPatch接入
- 注册
注册jspatch账号并登录.(http://www.jspatch.com).
- 添加应用,获取appKey
在"我的app"中添加新的app,未上线项目无需填写AppStoreID,添加完成后可以看到appKey. - 导入framework和相关类库
在官网下载SDK,将解压得到的JSPatch.framework
导入到项目,并添加依赖的类库:libz.dylib
和JavaScriptCore.framework
即可. - 测试补丁和发布补丁
新建一个main.js文件,在这个文件中编写js代码(用于替换oc代码的),这个main.js文件如果在项目中,通过[JSPatch testScriptInBundle];
方法测试补丁,补丁测试通过后,可以将这个main.js文件上传到jspatch官网,通过[JSPatch startWithAppKey:@"APPKey"];
方法调用在线补丁,项目中的appDelega.m文件中的代理方法中写入这个方法,发布的app每次启动时会在线查找补丁,如果有版本号一致的补丁,则会下载这个补丁,用来替换相应的方法. - 发布补丁包
在app管理中"新建版本",新建版本后,上传main.js文件再点击提交即可.需要注意的是新建版本中的版本,对应项目的版本号,app在线上寻找补丁时,版本号是一个筛选条件.
代码如下:
///本地测试
///AppDelegate.m
#import <JSPatch/JSPatch.h>
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
//本地测试版本
[JSPatch testScriptInBundle];
[JSPatch sync];
}
///线上补丁
///AppDelegate.m
#import <JSPatch/JSPatch.h>
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
//本地测试版本
[JSPatch startWithAppKey:@"APPKey"];
[JSPatch sync];
}
举个栗子
OC代码如下
///viewController.m
- (void)viewDidLoad {
[super viewDidLoad];
self.array = @[@"one",@"two",@"three"];
UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(135, 250, 100, 50)];
[button setBackgroundColor:[UIColor blueColor]];
[button addTarget:self action:@selector(buttonClicked:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
}
//按钮点击事件,模拟数组越界
-(void)buttonClicked:(UIButton *)sender{
UIAlertView *av = [[UIAlertView alloc] initWithTitle:@"提示" message:self.array[3] delegate:self cancelButtonTitle:nil otherButtonTitles:@"确定", nil];
[av show];
NSLog(@"%@",self.array[3]);
}
下边用js替换点击方法buttonClicked
///main.js
defineClass("ViewController", {
buttonClicked: function(sender) {
//1.获取/修改 Property: 等于调用这个 Property 的 getter / setter 方法,获取时记得加()
//2.OC中的nil -->jspatch中的null
//3.require('className')
//4.NSLog(@"123") -->console.log("123")
var message = self.array().objectAtIndex(0);
var temAlertView = require('UIAlertView').alloc().initWithTitle_message_delegate_cancelButtonTitle_otherButtonTitles("提示",message, self, "OK", null);
temAlertView.show()
console.log(message);
}
})
///AppDelegate.m
#import <JSPatch/JSPatch.h>
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
//本地测试版本
[JSPatch startWithAppKey:@"APPKey"];
[JSPatch sync];
}
然后上传这个main.js,发布补丁
关于安全问题
传输安全
JSPatch脚本的执行权限很高,若在传输过程中被中间人篡改,会带来很大的安全问题,为了防止这种情况出现,我们在传输过程中对JS文件进行了RSA签名加密,流程如下:
服务端:
计算 JS 文件 MD5 值。
用 RSA 私钥对 MD5 值进行加密,与JS文件一起下发给客户端。
客户端:
拿到加密数据,用 RSA 公钥解密出 MD5 值。
本地计算返回的 JS 文件 MD5 值。
对比上述的两个 MD5 值,若相等则校验通过,取 JS 文件保存到本地。
由于 RSA 是非对称加密,在没有私钥的情况下第三方无法加密对应的 MD5 值,也就无法伪造 JS 文件,杜绝了 JS 文件在传输过程被篡改的可能。
本地存储
本地存储的脚本被篡改的机会小很多,只在越狱机器上有点风险,对此 JSPatch SDK 在下载完脚本保存到本地时也进行了简单的对称加密,每次读取时解密。
自定义RSA Key
客户端和 JSPatch 后台默认有一对 RSA 密钥,默认会用这对密钥进行加解密验证。
若对安全要求较高,可以自定义 RSA 密钥,然后将公钥通过:
[JSPatch setupRSAPublicKey:@"******"]
方法写入app代码中,而私钥需要开发者自行保留,以后每次发布补丁的时候,需要同时上传这个私钥,如图:(生成秘钥的具体方法看官方文档,点这里)
这里上传的 rsa_private_key.pem 只是一次性使用,不会保存在服务端,所以只有通过用户自己保存的 rsa_private_key.pem 文件才可以针对 APP 下发脚本,即使 JSPatch 平台或者七牛云被黑,第三方也无法对你的 APP 下发恶意脚本(可以下发,但验证不过,不会执行),保证安全性。rsa_private_key.pem 请妥善保管,避免泄露。
使用自定义RSA秘钥的作用:
使用自定义的RSA秘钥,可以保障以下两种情况下的安全:
- 当jspatch账号被盗时,盗号者没有私钥,即使发布恶意补丁包,该补丁也不会被执行.(因为没有私钥,无法通过验证)
- 当jspatch服务器被黑,由于jspatch方面并不保留上传的私钥,所以app依然是安全的.
常见问题:发布补丁后不起作用
原因可能有:
1.APPKey写错了(好好检查).
错误信息:JSPatch: request success {error = "Document not found";}
2.发布的版本号和项目的版本号不一致(一定要一致,1.2版本的项目只能用1.2版本的补丁包)
错误信息:JSPatch: request success {error = "Document not found";}
3.main.js文件错误,是否找对了"类"和"方法"(类名和方法名要是写错,补丁想起作用也做不到啊)
错误信息:不会出现任何"JSPatch: request success {}"提示
4.main.js文件语法错误(main.js文件中的js语法错误,或者是oc方法名错误都会导致编译不通过,从而不执行main.js方法,最可怕的是他还不报错,jspatch的原则是,js文件能用就用,不能用,我不告诉你,就是不用.)(一个很长的oc方法转写成js方法是,是没有提示的,一个字母写错了,整个js文件就废了,最关键的是,这些错误的是隐形的,不提示,难发觉,更难找,所以我建议两点,第一,写补丁时现在本地测试,在本地的main.js文件中写一行测试一行,保证每一行都是正确的.第二,把要改的oc代码,暂时copy到main.js文件中,这样,当你在js中写oc的方法名时,而这个方法名在你copy的那段代码中,xcode就会给提示.)
错误信息:输出成功信息JSPatch: request success {v = 1;},但是依然执行oc代码而不是js代码
一点儿小总结
补丁发布后,app运行时会把补丁下载下来,一切验证通过后,补丁就可以运行了,这时候,如果你在jspatch管理补丁页面停用了补丁,下载过补丁的app还是会执行补丁.
那么问题来了,我们如何让已经下载过补丁的app不执行补丁呢?
最简单的方法是,先停用补丁,然后把app卸载后重新安装.但是总不能让客户先卸载再重新安装吧.
然后我做了以下几个实验:
实验假设: 我们已经发布补丁修改了代码中的方法 funA,下载过补丁的app执行补丁中的方法,不再执行原方法funA,现在如何让下载过补丁的app不再执行补丁,而是执行原来的方法呢?
实验1. 发布一个空白的补丁,也就新建一个空白的main.js文件,然后上传,企图用这个空白的补丁覆盖原来的.
结果: 失败了,下载过补丁的app依然执行补丁.
实验2. 把原来补丁里的方法名改掉,比如把funA 改成 funAXiugai,这样补丁找不到对应的方法,原方法就可以执行了.
结果: 失败了,下载过补丁的app依然执行补丁.
实验3. 在代码中有一个永不执行的(不被调用)方法funForeverNotDo,这个方法就是用来预防这种情况而存在的(好凄惨的备胎啊),然后写一个main.js,里边只修改funForeverNotDo方法,不再修改funA.
结果: 成功啦
更新
擦擦擦擦,打脸了,如果想要已经下载过补丁的app不再执行补丁,直接"删除版本"就可以了:
再更新
有一个神奇的网站,可以把OC代码翻译成JS代码:
传送门
Github上有本地源码:
Github本地源码