一站式搞定 JSPatch 热修复
最近接触到热修复, 确实能解燃眉之急, 非常好用, 故分享给大家. 这里只讲 JSPatch, 这个是现在最热门最好用的框架, 用起来超级简单, 非常感谢 bang590 的贡献.
JSPatch 是一个开源项目, 只需要在项目里引入极小的引擎文件, 就可以使用 JavaScript 调用任何 Objective-C 的原生接口, 替换任意 Objective-C 原生方法. 目前主要用于下发 JS 脚本替换原生 Objective-C 代码, 实时修复线上 bug.
项目集成
[Github][1] 下载后, 按照[操作文档][2]操作就可以轻松集成, 摘录 bang590 Github 简要步骤如下:
[1]:https://github.com/bang590/JSPatch
[2]:https://github.com/bang590/JSPatch/blob/master/README-CN.md
- 拷贝
JSPatch/
目录下的三个文件JSEngine.m
/JSEngine.h
/JSPatch.js
到项目里 #import "JPEngine.h"
- 调用
[JPEngine startEngine]
- 通过
[JPEngine evaluateScript:@""]
接口执行 JavaScript。 - 直接把下面代码拷贝到
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
方法中即可, app 运行后只调用一次, 即每次运行 app 只更新一次 JS 修复 - 如若要更新即时性, 可以把方法放到
- (void)applicationWillEnterForeground:(UIApplication *)application
这样每次 app 从后台进入前台, 都会拉取 JS 修复文件
// 方法一: 从网络拉回js脚本执行
[JPEngine startEngine];
[NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:kDownloadPath]] queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
NSString *script = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
[JPEngine evaluateScript:script];
}];
// 上面代码 kDownloadPath 换成你自己的 JS 文件地址即可
// 每次都从网络拉取, 虽然文件小, 但也受限也网络状态, 不太理想.
// 方法二: 先下载到本地, 再从本地文件夹中读取
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDownloadTask *task = [session downloadTaskWithURL:[NSURL URLWithString:kDownloadPath] completionHandler:^(NSURL * _Nullable location,NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSLog(@"location: %@", location);
// 下载任务会把下载的资源存放到临时文件夹tmp下. block结束后, 就会自动删除.
NSString *docPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSString *path = [docPath stringByAppendingPathComponent:@"demo.js"];
NSLog(@"path: %@", path);// 拷贝路径在 Finder ->前往 ->前往文件夹 可看到已下载文件
// 测试了会有缓存, 且不能把原有的 JS 文件覆盖, 故要先移除
if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
}
// 故把下载数据移动到document下
[[NSFileManager defaultManager] moveItemAtURL:location toURL:[NSURL fileURLWithPath:path]error:nil];
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[JPEngine startEngine];
[JPEngine evaluateScriptWithPath:path];
}];
}];
[task resume];
// 上面代码 kDownloadPath 换成你自己的 JS 文件地址即可
实际不需要每次都拉取, 该方法也只是暂缓措施, 下次迭代版本必须把上次 JS 修复的用原生解决, 这时需要有一个后台可以下发 JS 下载路径和管理脚本, 并且需要处理传输安全等部署工作.
JS 文件
JS 文件创建
-
使用 xcode 创建 JS 文件
xcode 创建 JS 文件.png -
使用 Sublime Text 工具创建 JS 文件, 同样后缀保存为 .js 即可
JS 语法
- 在 defineClass 里定义 OC 已存在的方法即可覆盖, 语法如下:
defineClass(classDeclaration, [properties,] instanceMethods, classMethods)
@param classDeclaration: 字符串,类名/父类名和Protocol
@param properties: 新增property,字符串数组,可省略
@param instanceMethods: 要添加或覆盖的实例方法
@param classMethods: 要添加或覆盖的类方法
// 例如:
require('UIDevice');
defineClass("ViewController", {
viewDidLoad: function() {
var model = UIDevice.currentDevice().model();
console.log(model);
if (UIDevice.currentDevice().systemVersion().floatValue() >= 9) {
console.log("9.0版本");
} else {
console.log("其他版本");
}
console.log("js 打印, 脚本号: 1.0, 替换实例成功");
}
}, {
test: function() {
console.log("js 打印, 脚本号: 1.0, 替换类方法成功");
}
});
-
要替换多个方法, 都要重新写
defineClass("类名", [新增属性,], {实例方法}, {类方法})
, 属性可以省略. -
只有类方法或者实例方法, 就留空大括号 {}, 如只需修改类方法:
defineClass("类名", {}, {类方法})
. -
在方法名前加 ORIG 即可调用未覆盖前的 OC 原方法:
viewDidLoad: function() { self.ORIGviewDidLoad(); },
- 在 JS 里面判断是否为空要判断 false
- ```java
var url = "";
var rawData = NSData.dataWithContentsOfURL(NSURL.URLWithString(url));
if (rawData != null) {} //这样判断是错误的
应该如下判断:
if (!rawData){}
在JSPatch.js源码里_formatOCToJS方法对undefined,null,isNil转换成了false。
- Objective-C 里的常量/枚举/宏/全局变量不能直接在 JS 上使用
- 更多语法见 JSPatch 基础语法, 也可以借助 JSPatch 代码转换器, 当然转换器不是万能了, 还需要自己细心检查.
- JSPatch 替换的是整个方法, 哪怕只有一行代码需要修复, 整个方法都需要重写成 JS 代码. 倡导使用敏捷开发的思想, 类似于主逻辑或者是功能模块入口的方法可以抽的更细, 这样即使需要修改, 成本也不会太大.
版本管理
公司搭建后台
自己公司搭建后台, 除了下发拉取 JS 的地址外, 还可以加入一些参数, 比如: 版本控制, 指定修复某 iOS 版本等等, 条件根据需求定, 跟一般请求无异, 就不叙述了.
七牛云平台
JS 文件也可以存放到七牛云上, 七牛云同样提供版本控制, 这样自己公司后台省很多事, 只需写一个接口, 而且有一定的免费额度, 足够用了.
七牛云平台.png七牛云使用流程
- 注册完七牛云账号后, 点击添加对象存储创建储存空间, 访问控制注意选公开空间, 这样外界才能访问到 JS 文件.
- 上传文件后, 复制外链接就是 JS 文件路径
- 需要注意的是七牛云平台文件是有缓存的, 所以在上传 JS 文件的命名不要和前面重复, 不然下发后看到结果会是上一次同名文件效果, 缓存时间可以在空间设置里设置
JSPatch 平台
不想搭建后台, 可以使用 JSPatch 平台, 也不用把 JS 文件上传到七牛云, 直接上传到 JSPatch 平台即可, 功能很多, 还提供条件下发, 平台文档介绍已经非常详细了, 这里就不再赘述了.
不过平台是需要收费的
JSPatch平台收费.png!!!使用 JSPatch 平台注意点
- 注意在 JSPatch 平台的规范里,JS 脚本的文件名必须是
main.js
。 - 自定义 RSA 密钥, 按照提示在终端输入命令后, 生成的文件在主目录下:
-
按照文档那样导入
public_key
太麻烦了, 而且容易出错, 可以把rsa_public_key.pem
文件拖入工程中, 再执行下面代码就可以:
NSString *keyPath = [[NSBundle mainBundle] pathForResource:@"rsa_public_key" ofType:@"pem"];
NSString *publicKey = [NSString stringWithContentsOfFile:keyPath encoding:NSUTF8StringEncoding error:nil];
NSLog(@"publicKey: %@", publicKey);
[JSPatch setupRSAPublicKey:publicKey];
//下方是 JSPatch 启动代码
[JSPatch startWithAppKey:@"19ed6339k440fa3ab"];
ifdef DEBUG
[JSPatch setupDevelopment];
endif
[JSPatch sync];
######集成错误录
- 若使用 XCode8 接入,需要在项目 Capabilities 打开 Keychain Sharing 开关,否则在模拟器下载脚本后会出现 `decompress error, md5 didn't match` 错误(真机无论是否打开都没问题)
- pod JSPatch 平台 SDK 完成并添加依赖库, 启动 `startWithAppKey` 和 `sync` 后报错
- ```objc
duplicate symbol _OBJC_METACLASS_$_JPEngine in:
/Users/issuser/Library/Developer/Xcode/DerivedData/ViewController-ajurqnqqgeaehfajwnvxgpblrcmz/Build/Intermediates/ViewController.build/Debug-iphonesimulator/ViewController.build/Objects-normal/x86_64/JPEngine.o
/Users/issuser/Library/Developer/Xcode/DerivedData/ViewController-ajurqnqqgeaehfajwnvxgpblrcmz/Build/Products/Debug-iphonesimulator/JSPatch/libJSPatch.a(JPEngine.o)
ld: 11 duplicate symbols for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
//原因是工程里有手动导入 JSPatch.h JSPatch.m 和 JSPatch.js 文件, 和 cocoapods 冲突了
小结
JSPatch 热修复集成简单吧, 难点在 JS 语法上, 没有语法提示, 写的时候更要细心.
如果没有效果的话, 检查 JS 语法是否正确, 也可以通过 Safari 的调试工具对 JS 进行断点调试, 详见 JS 断点调试, 还有是否执行之前缓存的文件.