是否有过这样的经历:新版本上线后发现有个严重的bug,可能会导致crash率激增,可能会使网络请求无法发出,这时能做的只是赶紧修复bug然后提交等待漫长的AppStore审核,再盼望用户快点升级,付出巨大的人力和时间成本,才能完成此次bug的修复。
使用JSPatch可以解决这样的问题,只需在项目中引入JSPatch,就可以在发现bug时下发JS脚本补丁,替换原生方法,无需更新APP即时修复bug。
JSPatch用iOS内置的JavaScriptCore.framework作为JS引擎,但没有用它JSExport的特性进行JS-OC函数互调,而是通过Objective-CRuntime,从JS传递要调用的类名函数名到Objective-C,再使用NSInvocation动态调用对应的OC方法。
首先简单介绍一下这个实例要实现的功能
新建一个工程,有三个视图
视图1 ,rootViewController,
视图2,firstViewControoler,
视图3,sceondViewControll. (可以给firstViewControoler和SecondViewController分别加上不同的标题和背景色用以便好区分)
源代码中,视图1中有一个按钮,点击按钮进入视图2,代码如下
- (void)enterNextView:(id)sender
{
FirstViewController* vc =[[FirstViewController alloc]init];
[self.navigationControllerpushViewController:vc animated:YES];
}
现在我们要定义一个js文件,通过JSPatch框架来实现,点击按钮进入的视图不是视图2而是视图3.
1.首先需要引入JSPatch插件。
通过CocoaPod引入JSPatch插件:
pod ‘JSPatch’
工程引入系统框架 JavaScriptCore.framework
2.编写js代码(demo.js)。
require('SecondViewController')//声明引用的object c中的类
defineClass(rootViewController, {
enterNextView: function(sender) {
var vc = SecondViewController.alloc().init()
self.navigationController().pushViewController_animated(vc, YES)
}
})
//defineClass覆盖rootViewController里原来的按钮点击事件enterNextView
3.加载js文件
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
- {
// Override point for customization after application launch.
[JPEngine startEngine];//启动JP引擎
//加载本地demo.js文件。
NSString *sourcePath = [[NSBundle mainBundle]pathForResource:@"demo" ofType:@"js"];
NSString *script = [NSString stringWithContentsOfFile:sourcePathencoding:NSUTF8StringEncoding error:nil];
[JPEngine evaluateScript:script];
return YES;
}
运行程序 ,这时点击按钮,执行的不是rootViewController里的enterNextView函数,而是demo.js文件里的enterNextView,界面也就从视图1进入了视图3界面而不是视图2界面。
注:此处为了方便实现加载本地的js文件 ,真正应用的时候,需要把这个js文件放在服务器上程序启动时通过以下代码加载。
[JPEngine startEngine]; //启动JP引擎
//下载服务器端js文件并加载到JPEngine
[NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://cnbang.net/bugfix.JS"]] queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
NSString *script = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (script) {
[JPEngine evaluateScript:script];
}
JSPatchLoader 负责根据版本号向服务端拉取 JSPatch代码,并对代码进行 RSA校验/解压/执行,整个校验原理在JSPatch部署安全策略 这篇文章里详细说明,不再复述。安全策略可参考http://blog.cnbang.net/tech/2879/.
1.安装
拷贝 Loader/ 目录下的文件到你的项目。
2.配置
1)设 JPLoader.h的rootUrl 为你的服务器地址。脚本文件在服务器的存放路径是${rootUrl}/${appVersion}/${patchFile}
2)自行生成 RSA 公钥私钥,替换 JPLoader.h 里的 publicKey 和 tools/pack.php 里的 privateKey。
3脚本打包
1 JSPatch脚本文件规则:可以有多个 js 文件,脚本内可以调用 include() 接口包含,没有目录层级,必须包含一个 main.js 文件作为入口。
2 在命令行使用 Loader/tools/pack.php脚本打包 JS 文件,由用户放到自己的服务器上给客户端下载。
示例
$ php pack.php main.js other.js
会在当前目录生成 v1.zip 文件,打包了所有 js 文件并包含了校验文件。也可以在最后通过 -o 指定输出文件名:
$ php pack.php main.js -o v2
脚本文件名代表当前 patch 版本,与后续的+updateToVersion:callback: 接口相关。
4 加载
下载/更新脚本
客户端在得知服务端脚本有更新时,调用 +updateToVersion:callback:接口下载对应版本的脚本。至于如何得知服务端脚本更新可以自行定义,可以另外加个请求每次唤醒时询问服务器,也可以在 APP 原有的请求里加上这个信息。
举个例子,客户端当前 App 版本号为1.0,上述配置 rootUrl 变量配为 http://localhost/JSPatch/,服务端告诉客户端最新脚本版本号为2,于是调用 [JPLoader updateToVersion:2callback:nil],这时会去请求 http://localhost/JSPatch/1.0/v2.zip这个文件并解压验证,保存到本地目录等待执行。
执行脚本
通过 +run 接口执行已下载到本地的 JSPatch 脚本文件,建议在程序启动的 -application:didFinishLaunchingWithOptions: 里第一句调用这个接口,防止调用后执行 JSPatch 脚本过程中其他线程同时在执行相关代码,导致意想不到的问题。
5测试
在脚本文件还没打包上传到服务器前,可以先把文件加入项目工程 bundle 进行测试,加入后调用 +runTestScriptInBundle 就会执行项目工程里的 main.js 文件,并且 JS 脚本里 include() 接口也可以正常使用。
(参考https://github.com/bang590/JSPatch/wiki/JSPatch-Loader-使用文档)
要在项目中使用JSPatch实现动态更新ios,需要以下步聚
1.引入JSPatch和JavaScriptCore.framework
2.实现js文件并放到服务端供终端加载
(具本的实现js到object c转换的语法和需要注意事项目可参考文档https://github.com/bang590/JSPatch/wiki)
3.在AppDelegate中加载服务端的js文件
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[JPEngine startEngine];
[NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://cnbang.net/bugfix.JS"]] queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
NSString *script = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (script) {
[JPEngine evaluateScript:script];
}
}];
….
return YES;
}
@end