其中 WaxPatch 和 JSPatch 是使用较广泛的两种热修复方案。而苹果 review guideline 提到只允通过 JavaScriptCore.framework
或 WebKit
执行脚本,因此 JSPatch 是真正被 Apple 官方支持的。此外鉴于 JavaScript
比 lua
语言更亲民,使用系统内置的 JavaScriptCore.framework
而无需内嵌lua脚本引擎来解释运行lua代码,JSPatch 便成为目前 iOS 热修复使用最多,效果也最佳的方案。
有关上述几种热修复方案的比较可阅读这两篇文章: Weex & ReactNative & JSPatch
JSPatch 使用时需要一个后台下发和管理脚本。阿里百川 HotFix 平台帮助开发者做了这些事。通过提供脚本托管、版本管理、脚本文件及传输过程加密等服务,让开发者无需搭建后台和关心部署操作,只需引入一个 SDK 即可直接使用 JSPatch 进行热修复。这个 SDK 就是 APatch(iOS)。
APatch 在 JSPatch 核心代码的基础上封装了向 HotFix 平台请求脚本/传输解密/脚本管理/本地调试等功能,是配合阿里百川 HotFix 平台一起使用的。
JSPatch 脚本执行权限很高,若被第三方篡改会 带来很大安全问题 。因此 APatch 和 HotFix 平台都对安全问题考虑良多。
从上图可看出,客户端从服务器下载 Patch 之前先要下载指定 Patch 配置信息即 PatchInfo
,其中包含了 Patch 文件密钥 file_token
。 服务端: - 对 file_token
用 RSA 公钥加密。 - 对 PatchInfo
原始数据采用 HMacSha1 算法计算的哈希值,并将原始数据和哈希值 serviceToken
放在同一消息中传送给客户端。
客户端: - 使用 secret
计算所接收数据的哈希值。 - 检查计算所得的 HMAC 是否与传送的 HMAC 匹配。 - 只有 PatchInfo
通过校验匹配后才会去下载 Patch
。
另外,update patch 的接口已迁至 https,进一步保证了数据传输的安全。
本地存储的脚本被篡改的机会小很多,只在越狱机器上有点风险,对此 APatch SDK 对下载的脚本进行了AES对称加密,每次读取时: - 客户端使用 RSA 私钥解密 PatchInfo.file_token
获取 key
和 iv
。 - 使用 key
和 iv
进行 AES 解密。
解密成功后的数据存储在 script
中,然后会调用 JSPatch 运行js脚本的接口:
[JPEngine evaluateScript:script];
至此,APatch 的工作已经完成,接下来具体的热修复工作就交给 JSPatch 了。
JSPatch 是一个开源项目( Github链接 ),只需要在项目里引入极小的引擎文件,就可以使用 JavaScript 调用任何 Objective-C 的原生接口,替换任意 Objective-C 原生方法。目前主要用于下发 JS 脚本替换原生 Objective-C 代码,实时修复线上 bug。 ——JSPatch wiki
“极小的引擎文件”指的就是 JavaScriptCore。OS X Mavericks 和 iOS 7 引入了 JavaScriptCore 库,它把 WebKit 的 JavaScript 引擎用 Objective-C 封装,提供了简单、快速、安全的方式接入世界上最流行的语言: - 在 Objective-C 代码中直接执行 JavaScript 代码段; - 在 JavaScript 语言环境里调用 Objective-C 公开给 JavaScript的 方法; - 内存管理和线程封装。
如果未接触过 JavaScriptCore,在深入学习 JSPatch 之前有必要先了解一下这个js引擎怎么使用。
JSContext
是运行 JavaScript 代码的环境。可以在 JSContext
中创建变量、计算、定义方法等:
JSContext *context = [[JSContext alloc] init]; [context evaluateScript:@"var num = 5 + 5"]; [context evaluateScript:@"var names = ['Grace', 'Ada', 'Margaret']"]; [context evaluateScript:@"var triple = function(value) { return value * 3 }"];
JSValue
包装了每一个可能的 JavaScript 值,任何出自 JSContext
的值都被包裹在一个 JSValue
对象中:
JSValue *tripleNum = [context evaluateScript:@"triple(num)"]; //取出jsvalue中的值 NSLog(@"Tripled: %d", [tripleNum toInt32]);//30
对 JSContext
和 JSValue
实例使用下标可以访问之前创建的 context 的任何值。 JSContext
需要一个字符串下标, JSValue
使用字符串或整数下标来得到里面的对象和数组:
JSValue *names = context[@"names"]; JSValue *initialName = names[0]; NSLog(@"The first name: %@", [initialName toString]);//Grace
调用JS方法需要使用 callWithArguments:
传递参数:
JSValue *tripleFunction = context[@"triple"]; JSValue *result = [tripleFunction callWithArguments:@[@5]]; NSLog(@"five tripled:%d",[result toInt32]);
这里使用 Foundation 类型 NSArray
作为参数来直接调用该函数。JavaScriptCore 可以 很轻松地处理这个桥接。 以上js代码都以字符串形式直接出现在oc代码中,实际中也可以在项目中引入.js文件,执行js文件中的内容。即:
NSString *path = [[NSBundle bundleForClass:[self class]] pathForResource:@"main" ofType:@"js"]; NSString *jsCore = [[NSString alloc] initWithData:[[NSFileManager defaultManager] contentsAtPath:path] encoding:NSUTF8StringEncoding]; [context evaluateScript:jsCore];
从JS访问在OC中定义的对象和方法有两种方式:
JSContext
注册 NSBlock
对象: context[@"add"] = ^(NSInteger a, NSInteger b) { NSLog(@"add result:%@", @(a + b)); }; context.exceptionHandler = ^(JSContext *con, JSValue *exception) { NSLog(@"%@", exception); con.exception = exception; //异常处理... }; [context evaluateScript:@"add(2,3)"];//5
JSExport
协议: 定义一个 Test
类,遵循 JSExport
协议:
// in Test.h ----------------- //定义一个JSExport子协议,暴露OC方法定义 @protocol TestJSExports <JSExport> - (void)log:(id)value; - (void)addX:(int)x withY:(int)y; @end @interface Test : NSObject <TestJSExports> - (void)callOC; @end // in Test.m ----------------- @implementation Test - (void)callOC { JSContext *context = [[JSContext alloc] init]; //将实现了上面定义的协议的对象设置给JSContext context[@"Test"] = self; //执行在JSContext中的JS代码,即可以执行传入的对象的JSExport协议中定义的方法 [context evaluateScript:@"Test.log('Hello JavaScript')"]; [context evaluateScript:@"Test.addXWithY(1, 2);"]; } - (void)log:(id)value { NSLog(@"value = %@", value); } - (void)addX:(int)x withY:(int)y { NSLog(@"x + y = %d", x + y); } @end
测试:
Test *test = [[Test alloc]init]; [test callOC]; //"value = Hello JavaScript","x + y = 3"