例如线上 APP 有一段代码出现 bug 致使 crash:前端
@implementation JPTableViewController ... - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { NSString *content = self.dataSource[[indexPath row]]; //可能会超出数组范围致使crash JPViewController *ctrl = [[JPViewController alloc] initWithContent:content]; [self.navigationController pushViewController:ctrl]; } ... @end
能够经过下发这样一段 JS 代码,覆盖掉原方法,修复这个 bug:git
//JS
defineClass("JPTableViewController", {
//instance method definitions
tableView_didSelectRowAtIndexPath: function(tableView, indexPath) {
var row = indexPath.row()
if (self.dataSource().length > row) { //加上判断越界的逻辑
var content = self.dataArr()[row];
var ctrl = JPViewController.alloc().initWithContent(content);
self.navigationController().pushViewController(ctrl);
}
}
pod 'JSPatchPlatform'
pod install
便可。
JSPatchPlatform.framework
拖入项目中,勾选 "Copy items if needed",并确保 "Add to target" 勾选了相应的 target。
libz.dylib
和
JavaScriptCore.framework。
AppDelegate.m
里载入文件,并调用
+startWithAppKey:
方法,参数为第一步得到的 AppKey。接着调用
+sync
方法检查更新。例子:
decompress error, md5 didn't match
错误(真机不管是否打开都没问题):
-application:didFinishLaunchingWithOptions:
开头处调用。
+startWithAppKey:
并不会询问后台 patch 更新,必须调用
+sync
方法。
+sync
就会请求一次后台,对于实时性要求不高的 APP,只需在
-application:didFinishLaunchingWithOptions:
处调用一次,这样用户会在启动时去同步 patch 信息。对于实时性要求高的 APP,能够在
-applicationDidBecomeActive:
处调用这个接口,这样会在每次用户唤醒 APP 时去同步一次后台,请求次数会增多,但有 patch 更新时用户会及时收到。
NSLog()
打出,若你的 APP 有本身的日志系统,但愿把 log 打在你的日志系统里,能够在调用 +startWithAppKey 以前调用这个接口:
[JSPatch setLogger:^(NSString *msg) {
//msg 是 JSPatch log 字符串,用你自定义的logger打出
YOUR_APP_LOG(@"%@", msg);
}];
+startWithAppKey:
方法,测试完成后须要删除。
typedef NS_ENUM(NSInteger, JPCallbackType){ JPCallbackTypeUnknow = 0, JPCallbackTypeRunScript = 1, //执行脚本 JPCallbackTypeUpdate = 2, //脚本有更新 JPCallbackTypeUpdateDone = 3, //已拉取新脚本 JPCallbackTypeCondition = 4, //条件下发 JPCallbackTypeGray = 5, //灰度下发 };
举例:github
[JSPatch setupCallback:^(JPCallbackType type, NSDictionary *data, NSError *error) {
switch (type) {
case JPCallbackTypeUpdate: {
NSLog(@"updated %@ %@", data, error);
break;
}
case JPCallbackTypeRunScript: {
NSLog(@"run script %@ %@", data, error);
break;
}
default:
break;
}
}];
+sync:
以前调用,用于条件下发,例如:
[JSPatch setupUserData:@{ @"userId": @"100867", @"location": @"guangdong" }];
+sync:
以前调用,详见
自定义 RSA 密钥。
+setupDevelopment
的客户端生效。
DEBUG
时设置,详见
开发预览。
首先项目必须接入 JSPatch SDK,并关联 AppKey,线上版本必须带有这个 SDK。web
假设已接入 JSPatch SDK 的某线上 APP 发现一处代码有 bug 致使 crash:编程
上述代码中取数组元素处可能会超出数组范围致使 crash,对此咱们写了以下 JS 脚本准备替换上述方法修复这个 bug:数组
注意在 JSPatch 平台的规范里,JS脚本的文件名必须是 main.js
。接下来就看如何把这个 JS 脚本下发给全部用户。浏览器
在上线以前须要对脚本进行本地测试,看看运行是否正常。SDK 提供了方法 +testScriptInBundle
用于发布前的测试:七牛云存储
调用这个方法后,JSPatch 会在当前项目的 bundle 里寻找 main.js 文件执行,效果与最终线上用户下载脚本执行同样,测试完后就能够准备上线这个脚本。缓存
注意 +testScriptInBundle
不能与 +startWithAppKey:
一块儿调用,+testScriptInBundle
只用于本地测试,测试完毕后须要去除。安全
进入 JSPatch 平台后台,在个人 APP 里选择这个 APP,点击添加版本。填入当前线上 APP 的版本号,能够在项目 TARGETS -> General -> version 上能够找到:
注意这里版本号必须一致,JSPatch 平台会只针对这个版本号下发对应的 JS 脚本,若版本号对应不上,客户端也就请求不到相应的 JS 脚本。
点击进入刚添加的版本,上传 main.js
便可。
上传能够直接全量下发,也能够选择 开发预览 或 灰度或条件下发,也可使用自定义 RSA key 对脚本进行加密签名。
上传完成后,对应版本的 APP 会请求下载这个脚本保存在本地,之后每次启动都会执行这个脚本。至此线上 bug 修复完成。
若后续须要对这个脚本进行修改,能够从新上传新的脚本,APP 客户端会在请求时发现脚本已更新,下载最新脚本覆盖原来的,下次启动时执行。
JPDispatch: 提供完整GCD接口 JPLocker: 提供@synchronized接口 JPNumber: 包装 NSNumber JPProtocol: 提供@protocol接口 JPSpecialInit: 特殊类 UIWebview 和 NSCalendar 的初始化
pod 'JSPatchPlatform' pod 'JSPatchPlatform/Extensions' pod 'JSPatchPlatform/JPCFunction'
pod install
即完成接入。
服务端:
客户端:
openssl
,再执行如下三句命令,生成 PKCS8 格式的 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 -pubout -out rsa_public_key.pem
rsa_private_key.pem
和
rsa_public_key.pem
这两个文件。这里生成了长度为 1024 的私钥,长度可选 1024 / 2048 / 3072 / 4096 ...。
+setupRSAPublicKey:
设置自定义的 RSA Public Key,注意应该在
+sync
以前调用,由于
+sync
可能会下载到脚本,这时已经要用 RSA key 去验证了。
\n
,例:
//rsa_public_key.pem -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApgeqKYKPVFk1dk2JGrKv EaSqqXxU2S1x32xn2M2jWK/lz7YOPRFcPhH8UgBgpUQGqbW2ooOrtlE0Ur6WHOgZ HvozA71xKEgpQhLbX8ourcyC638zfEQJ3aUezjy5ADzlIAWr3ayBYmLBYj4OkRRG bffxwA+i16jNVFWJFzgCrRs44cpn+nX0VsNrNjntt59J3xIhMGE+eQ2K9WDwYmv4 sw8+3MsW++z2Uornmi9v2atZnBKd/dBsGz05d++NBks7b2ot/TAiMRnit+VNTZrs 1rYQOcoCJlMUK4GDkK6bdKAPfVcD5vy2PAxDA84P2txcSkFozmZABcVvSyASB6Bn MQIDAQAB -----END PUBLIC KEY-----
[JSPatch setupRSAPublicKey:@"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApgeqKYKPVFk1dk2JGrKv\nEaSqqXxU2S1x32xn2M2jWK/lz7YOPRFcPhH8UgBgpUQGqbW2ooOrtlE0Ur6WHOgZ\nHvozA71xKEgpQhLbX8ourcyC638zfEQJ3aUezjy5ADzlIAWr3ayBYmLBYj4OkRRG\nbffxwA+i16jNVFWJFzgCrRs44cpn+nX0VsNrNjntt59J3xIhMGE+eQ2K9WDwYmv4\nsw8+3MsW++z2Uornmi9v2atZnBKd/dBsGz05d++NBks7b2ot/TAiMRnit+VNTZrs\n1rYQOcoCJlMUK4GDkK6bdKAPfVcD5vy2PAxDA84P2txcSkFozmZABcVvSyASB6Bn\nMQIDAQAB\n-----END PUBLIC KEY-----"];
下发脚本时在发布脚本界面勾选 使用自定义RSA Key
选项,会出现文件上传框,选择本地的 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
请妥善保管,避免泄露。
+sync
以前调用
setupDevelopment
方法,建议只在 debug 模式下开启:
[JSPatch startAppWithKey:@""]; #ifdef DEBUG [JSPatch setupDevelopment]; #endif [JSPatch sync];
开发预览
,就能够在 debug 模式下测试这个补丁。测试完成后能够选择全量下发或灰度/条件下发,下发给现网用户。
SDK 1.2
版本开始支持脚本的灰度与条件下发。
userId==10000876
,
iOS>9.0&&isMale==1
。
条件语句里用到的 key/value 须要事先在 APP 里经过 +setupUserData:
设置,支持设置多个字段,用 NSDictionary 表示,例如能够设置当前登陆的用户ID以及性别:
//_userId = @"1000876" //_isMale = @(1) [JSPatch setupUserData:@{@"userId": _userId, @"isMale": _isMale}];
这样在下发脚本时填入条件 userId==1000876
后,这个脚本就只对这个用户生效,若是填入 isMale==0
则对这个用户不生效,对其余在 SDK 设置了 @"isMale": @"0"
的用户生效。
&&
||
==
!=
>=
<=
>
<
,意思跟程序里同样。>=
<=
>
<
会把值转为数值进行对比。例如 userId>200000
,即便客户端调用 +setupUserData:
接口时设置的 userId 字段是字符串,也会转为数值进行对比。==
!=
符号时,会以字符串形式判断是否相等,例如 1.0 == 1
结果是 NO。location!=guangdong
userId!=31242&&location==guangdong&&name==bang
&&
和 ||
,&&
的优先级较高。例如 userId<200000||location==guangdong&&name==bang
,会先分别计算 userId<200000
和 location==guangdong&&name==bang
的结果,再进行 ||
运算。在发布脚本1时设条件为 userId==1000876
,某设备A设置了 @{@"userId": @"1000876"}
命中了这个条件,执行了这个脚本1。设备B设置了 @{@"userId": @"2000876"}
没有命中。
接着在后台修改条件为 userId>=2000000
,设备A并不符合这个条件,但由于以前的条件命中过,因此设备A不会再受这个改变影响,继续执行脚本1。设备B命中了这个条件,也执行了脚本1。
此外若想撤销条件全量发布,提交空条件便可。
iOS
和
isPad
,分别表示 iOS 版本号和是否iPad,不须要设置就能够拿这两个字段用于条件判断。
例如只针对 iOS8 的 iPad 下发,能够直接写这个条件:iOS>=8.0&&iOS<9.0&&iPad==1
。
注意 iOS 版本号只会精确到两位,例如 9.2.1 会记录成 9.2,iOS==9.2
会命中 9.2.x 版本。
+setupUserData:
接口要在 +sync:
接口以前调用。SDK 1.1
及如下版本会无视任何条件和灰度值,直接全量接收。参数名:name
参数值:bang
。
+application:didFinishLaunchingWithOptions:
里调用
+updateConfigWithAppKey:
方法,传入
appKey
,APP 就会在调用处发请求获取刚才设置的在线参数。
+getConfigParams
拿到全部参数,也能够经过
+getConfigParam:
接口拿到单个参数,例如:
NSDictionay *configs = [JSPatch getConfigParams]; //configs == @{@"name": @"bang"} NSString *name = [JSPatch getConfigParam:@"name"]; //name == bang
+updateConfigWithAppKey:
的请求返回时进行一些操做,能够经过
+ setupUpdatedConfigCallback:
接口设置 callback:
[JSPatch setupUpdatedConfigCallback:^(NSDictionary *configs, NSError *error) { NSLog(@"%@ %@", configs, error); }];
为了不重复请求浪费资源,默认 +updateConfigWithAppKey:
接口请求时间间隔至少为30分钟,也就是30分钟内屡次调用 +updateConfigWithAppKey:
只会请求一次。若想 APP 对在线参数响应更实时,能够经过 +setupConfigInterval:
接口修改这个间隔值。
+updateConfigWithAppKey:
方法算一次请求。[JSPatch sync]
时从新拉取,重试 3 次失败后,会上报失败数据,在实时监控这里也能够看到每一条失败数据以及对应的错误码,方便排查问题。
POST http://jspatch.com/Apps/uploadPatch @params email 登陆邮箱 @params password 登陆密码 @params appKey APP惟一键值 @params appVersion APP版本号 @params gray (可选)灰度策略,值为1-9,表明10%-90% @params condition (可选)条件下发 @params patch[] 补丁文件 @params rsaKey (可选)rsa private密钥文件 //失败返回 @return {errMsg: ''} //成功返回 @return {succ: 1, patchVersion: {$patchVersion}}
curl -F 'email=test@qq.com' -F 'password=test1234' -F 'appKey=2ba21d234fa69915' -F 'appVersion=2.0' -F 'gray=4' -F 'patch[]=@main.js' http://jspatch.com/Apps/uploadPatch
POST http://jspatch.com/Apps/updatePatch @params email 登陆邮箱 @params password 登陆密码 @params appKey APP惟一键值 @params appVersion APP版本号 @params gray (可选)修改灰度策略,值为1-9,表明10%-90% @params condition (可选)修改条件下发规则 @params all (可选)修改成全量下发 //失败返回 @return {errMsg: ''} //成功返回 @return { succ: 1, patch: { patchID: 5804, gray: 3, condition:null, isDev:0 } }
curl -F 'email=test@qq.com' -F 'password=test1234' -F 'appKey=2ba21d234fa69915' -F 'appVersion=2.0' -F 'condition=userId=21' http://jspatch.com/Apps/updatePatch
NSLog('xx')
,应该用 console.log('xx')
self.navigationItem()
,而不是 self.navigationItem
self.valueForKey()
和 self.setValue_forKey()
接口存取。self
+testScriptInBundle
接口执行脚本看有没有问题,(详情参照
使用范例),若没达到预期效果,能够一步步调试,第一步请在
main.js
开头打
console.log('run success')
,肯定 XCode 控制台有输出这条 log,肯定脚本有被执行到,再进行其余调试。通常调试使用
console.log()
就足够,如有更多需求能够用
Safari断点调试。
appKey
和
版本号
没有错误。
2016-04-27 19:04:42.212 ... JSPatch: runScript 2016-04-27 19:04:42.399 ... JSPatch: evaluated script, length: 28
2016-04-27 19:04:42.399 ... JSPatch: request http://7xkfnf.com1.z0.glb.clouddn.com/6d2fddf24c5d8af2/1.0?v=1461755082.399732 2016-04-27 19:04:42.621 ... JSPatch: request success { v = 2; }
这两句表示请求到了当前版本补丁版本号,这里 url 里的 6d2fddf24c5d8af2
是 appkey
,后续跟的 1.0
是 App 版本号,能够检查下这两个值是否正确。若 url 不正确或者脚本没有正确上传,这里会返回 error = "Document not found"
。
--
2016-04-27 19:09:43.798 ... JSPatch: updateToVersion: 2 2016-04-27 19:09:43.798 ... JSPatch: request file http://7xkfnf.com1.z0.glb.clouddn.com/6d2fddf24c5d8af2/1.0/file2 2016-04-27 19:09:43.900 ... JSPatch: request file success, data length:3072 2016-04-27 19:09:43.908 ... JSPatch: updateToVersion: 2 success
这几句表示检测到的补丁版本号比本地版本更新,去下载补丁文件,下载后会当即执行,到这一步应该就没问题了。若这个版本的补丁以前已经下载过,就不会再下载。
新建 APP 版本 -> 上传补丁 -> 删除APP版本 -> 新建同一个APP版本 -> 上传补丁
,会中缓存逻辑,致使请求到的脚本是删除 APP 以前上传的旧补丁。这时只须要再上传一次补丁更新版本号就能够了。
decompress error, md5 didn't match
错误(真机不管是否打开都没问题):