(一)什么是 JSPatch
JSPatch 是一个开源项目(Github链接),只需要在项目里引入极小的引擎文件,就可以使用 JavaScript 调用任何 Objective-C 的原生接口,替换任意 Objective-C 原生方法。目前主要用于下发 JS 脚本替换原生 Objective-C 代码,实时修复线上 bug。
例如线上 APP 有一段代码出现 bug 导致 crash:
(二)SDK 接入
- 使用 Cocoapods 方法,直接集成
target :'GSJSPatchDemo'do
pod 'JSPatchPlatform'
end
-
添加依赖框架:TARGETS -> Build Phases -> Link Binary With Libraries -> + 添加 libz.dylib 和 JavaScriptCore.framework。
(三)生成和配置RSA秘钥
在 Mac 终端上执行 openssl,再执行以下三句命令,生成 PKCS8 格式的 RSA 公私钥,执行过程中提示输入密码,密码为空(直接回车)就行。
1.生成RSA秘钥
openssl
genrsa -out rsa_private_key.pem 1024
pkcs8 -topk8 -inform PEM -in rsa_private_key.pem -outform PEM –nocrypt
rsa -in rsa_private_key.pem -out rsa_public_key.pem -pubout
2.集成代码中
把 rsa_public_key.pem 的内容换行手动改成 \n,并放入 [JSPatch setupRSAPublicKey:@""] 里:
[JSPatch startWithAppKey:@"7f1cf281ed7e0480"];
[JSPatch setupRSAPublicKey:@"-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDAFQPfhYbUoUFldY8DlXs9U+IA\nacxYuXtM7jmzP016AoDcK1l4c5aC42Zo41yrdF64T2b6ubedjCw15WNgneVrD0Mj\nkAm7Sj3sxvb7t+SFVK016IB8oa7kmxYCcTYNT9BIoAPeREFuvBk8IC0x6Kn4tZnY\ntw8zm4/5ubYEvvE/IQIDAQAB\n-----END PUBLIC KEY-----"];
[JSPatch sync];
注意应该在 +sync 之前调用,因为 +sync 可能会下载到脚本,这时已经要用 RSA key 去验证了。
3.上传脚本
使用私钥下发补丁下发脚本时,选择本地的 rsa_private_key.pem 文件,与脚本一同上传,JSPatch 平台会使用这个上传的 Private Key 对脚本 MD5 值进行加密,再下发给客户端。若客户端经过上述第二步设置了对应的 Public Key,就会用设置的 Public Key 对脚本进行验证,验证通过后运行脚本,否则不会运行。
这里上传的 rsa_private_key.pem 只是一次性使用,不会保存在服务端,所以只有通过用户自己保存的 rsa_private_key.pem 文件才可以针对 APP 下发脚本,即使 JSPatch 平台或者七牛云被黑,第三方也无法对你的 APP 下发恶意脚本(可以下发,但验证不过,不会执行),保证安全性。rsa_private_key.pem 请妥善保管,避免泄露。
若担心此处私钥在网络传输中暴露,可以使用JSPatch 补丁打包加密工具,在本地进行加密打包,无需上传私钥即可下发脚本。
3.1 添加版本
App版本号在项目 TARGETS -> General -> version 上可以找到:
注意这里版本号必须一致,JSPatch 平台会只针对这个版本号下发对应的 JS 脚本,若版本号对应不上,客户端也就请求不到相应的 JS 脚本。
补丁文件:上传 main.js(注意在 JSPatch 平台的规范里,JS脚本的文件名必须是 main.js)
补丁描述:填不填都可以
RSA秘钥:选择本地的 rsa_private_key.pem 文件
上传完成后,对应版本的 APP 会请求下载这个脚本保存在本地,以后每次启动都会执行这个脚本,至此脚本下发完成。
3.2 删除、修改JS脚本,后方法同3.1类似
3.3 补丁测试
本地测试分两个步骤:
1.把补丁 main.js 拖入项目。
2.注释掉所有 JSPatch 相关方法,调用 +testScriptInBundle 方法:
#import <JSPatchPlatform/JSPatch.h>
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[JSPatch testScriptInBundle];
}
测试无误后,删除[JSPatch testScriptInBundle]代码即可
(四)使用方法
1、错误排查
1.1 JS脚本问题
如果 JS 脚本执行出错,XCode 控制台会打出错误日志,但仅靠错误日志可能难以发现,建议在 +startWithAppKey: 之前加入以下代码:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[JSPatch setupCallback:^(JPCallbackType type, NSDictionary *data, NSError *error) {
if (type == JPCallbackTypeJSException) {
NSAssert(NO, data[@"msg"]);
}
}];
[JSPatch startWithAppKey:@"7f1cf281ed7e0480"];
}
这样在 debug 阶段 JS 脚本执行错误时会中 assert,马上可以看到错误信息。
附几个常犯的错误:
- 1.不能用 NSLog('xx'),应该用 console.log('xx')
- 2.get property 记得加括号,例如 self.navigationItem(),而不是 self.navigationItem
- 3.私有成员变量要用 self.valueForKey() 和 self.setValue_forKey() 接口存取。
- 4.block 里不能直接使用 self
1.2 配置问题
1.先确保 SDK 接入时 appKey 填写正确(SDK接入文档),以及发布补丁时 版本号 没有错误。
2.分清 开发预览 模式和 正式下发 的区别,详见 开发预览。
3.根据 log 排查问题:
2018-08-09 11:01:20.860914+0800 GSJSPatchDemo[11192:7056572] JSPatch: 尝试加载补丁
2018-08-09 11:01:21.016938+0800 GSJSPatchDemo[11192:7056572] JSPatch.log: run success
2018-08-09 11:01:21.021863+0800 GSJSPatchDemo[11192:7056572] JSPatch: 补丁已加载, 文件大小: 146
打印上面log表示执行了脚本
2018-08-09 11:01:21.077655+0800 GSJSPatchDemo[11192:7056572] JSPatch: sync请求:https://dn-jspatch.qbox.me/7f1cf281ed7e0480/1.0.1?d=E9B55FD4-FCFC-4B0A-8D63-3645C4E6D3F4&sv=1.6.6&v=1533783681.077580
2018-08-09 11:01:21.109747+0800 GSJSPatchDemo[11192:7056572] INFO: Reveal Server started (Protocol Version 32).
2018-08-09 11:01:21.431226+0800 GSJSPatchDemo[11192:7056796] JSPatch: sync请求成功:{
v = 3;
}
上面的日志表示请求到了版本号,url里的7f1cf281ed7e0480就是我们的appkey,1.0.1就是我们APP版本号,项目要和JSPatch平台保存一致。若 url 不正确或者脚本没有正确上传,这里会返回 error = "Document not found"。
2018-08-09 11:01:21.431733+0800 GSJSPatchDemo[11192:7056796] JSPatch: 补丁版本没有更新
执行到这里,表示检测到的补丁版本号比本地版本更新,去下载补丁文件,下载后会立即执行,到这一步应该就没问题了。若这个版本的补丁之前已经下载过,就不会再下载。
- 查看更多日志信息
在代码里面加入[JSPatch showDebugView]; 这句代码,就会在自己的APP界面上出现一个debug面板
点击可以查看详细信息
上图的红框就是我们的mian.js脚本,和JSPatch平台上传的一致
2.执行测试:
#import "GPViewController.h"
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor yellowColor];
_redBtn = [[UIButton alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
_redBtn.backgroundColor = [UIColor redColor];
[_redBtn addTarget:self action:@selector(redBtnClick:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:_redBtn];
}
- (void)redBtnClick:(id)sender {
}
看到我们的代码redBtnClick这个方法并没有执行任何代码
现在点击红色View看到Xcode控制台输出
2018-08-09 11:15:19.541670+0800 GSJSPatchDemo[11192:7056572] JSPatch.log: 我进来了
也就是上面的main.js 里面的代码执行了。。。