一、什么是 JSPatch?
JSPatch 是一个开源项目( Github链接 ),只需要在项目里引入极小的引擎文件,就可以使用 JavaScript 调用任何 Objective-C 的原生接口,替换任意 Objective-C 原生方法。目前主要用于下发 JS 脚本替换原生 Objective-C 代码,实时修复线上 bug。例如线上 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:
//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);
}
}
}, {})
除了修复 bug,JSPatch 也可以用于动态运营,实时修改线上 APP 行为,或动态添加功能。JSPatch 详细使用文档见 Github Wiki。
二、什么是 JSPatch 平台?
JSPatch 需要使用者有一个后台可以下发和管理脚本,并且需要处理传输安全等部署工作,JSPatch 平台帮你做了这些事,提供了脚本后台托管,版本管理,保证传输安全等功能,让你无需搭建一个后台,无需关心部署操作,只需引入一个 SDK 即可立即使用 JSPatch。
三、JsPatch实现的内部原理
JsPatch能做到通过JS调用和改写OC方法最根本的原因是 Objective-C 是动态语言,OC上所有方法的调用/类的生成都通过 Objective-C Runtime 在运行时进行,我们可以通过类名和方法名反射得到相应的类和方法,也可以替换某个类的方法为新的实现,还可以新注册一个类,为类添加方法。所以 JSPatch 的原理就是:JS传递字符串给OC,OC通过 Runtime 接口调用和替换OC方法。这个很容易理解,JS的作用只是一个信使的作用,具体实现还是得靠我们OC。
同时在这里给大家一个比较好的网站 点这里.这是一个OC转JS的一个工具网站。
四、SDK接入
- 第一步 获得 AppKey
在平台上注册帐号,可以任意添加新 App,每一个 App 都有一个唯一的 AppKey 作为标识。
- 第二步 集成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 补丁,不需要等用户下次启动:
#import <JSPatchPlatform/JSPatch.h>
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[JSPatch startWithAppKey:@"你的AppKey"];
...
}
- (void)applicationDidBecomeActive:(UIApplication *)application {
[JSPatch sync];
...
}
@end
常见问题
若使用 XCode8 接入,需要在项目 Capabilities 打开 Keychain Sharing 开关,否则在模拟器下载脚本后会出现 decompress error, md5 didn't match 错误(真机无论是否打开都没问题):
五、使用范例
首先项目必须接入 JSPatch SDK,并关联 AppKey,线上版本必须带有这个 SDK。
假设已接入 JSPatch SDK 的某线上 APP 发现一处代码有 bug 导致 crash:
@implementation XRTableViewController
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *content = self.dataSource[[indexPath row]]; //可能会超出数组范围导致crash
XRViewController *controller = [[JPViewController alloc] initWithContent:content];
[self.navigationController pushViewController:controller];
}
@end
上述代码中取数组元素处可能会超出数组范围导致 crash,对此我们写了如下 JS 脚本准备替换上述方法修复这个 bug:
//main.js
defineClass("XRTableViewController", {
tableView_didSelectRowAtIndexPath: function(tableView, indexPath) {
var row = indexPath.row()
if (self.dataSource().length > row) { //加上判断越界的逻辑
var content = self.dataArr()[row];
var controller = XRViewController.alloc().initWithContent(content);
self.navigationController().pushViewController(controller);
}
}
})
注意在 JSPatch 平台的规范里,JS脚本的文件名必须是 main.js
。接下来就看如何把这个 JS 脚本下发给所有用户。
测试
在上线之前需要对脚本进行本地测试,看看运行是否正常。SDK 提供了方法 +testScriptInBundle
用于发布前的测试:
#import <JSPatch/JSPatch.h>
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[JSPatch testScriptInBundle];
….
}
@end
调用这个方法后,JSPatch 会在当前项目的 bundle 里寻找 main.js 文件执行,效果与最终线上用户下载脚本执行一样,测试完后就可以准备上线这个脚本。
注意 +testScriptInBundle
不能与 +startWithAppKey:
一起调用,+testScriptInBundle
只用于本地测试,测试完毕后需要去除。
添加版本
进入 JSPatch 平台后台,在我的 APP 里选择这个 APP,点击添加版本。填入当前线上 APP 的版本号,可以在项目 TARGETS -> General -> version 上可以找到:
注意这里版本号必须一致,JSPatch 平台会只针对这个版本号下发对应的 JS 脚本,若版本号对应不上,客户端也就请求不到相应的 JS 脚本。
添加JS脚本
点击进入刚添加的版本,上传 main.js
即可。
上传可以直接全量下发,也可以选择 开发预览 或 灰度或条件下发,也可以使用自定义 RSA key 对脚本进行加密签名。
上传完成后,对应版本的 APP 会请求下载这个脚本保存在本地,以后每次启动都会执行这个脚本。至此线上 bug 修复完成。
修改/删除JS脚本
若后续需要对这个脚本进行修改,可以重新上传新的脚本,APP 客户端会在请求时发现脚本已更新,下载最新脚本覆盖原来的,下次启动时执行。
若想直接取消某个 APP 版本的 JS 脚本补丁,可以直接在 APP 版本界面删除此 APP 版本,APP 客户端会在请求时发现脚本已被删除,即刻删除本地 JS 脚本文件,下次启动时不再加载。
参考:JSPatch 平台、http://blog.cnbang.net/tech/2808/