iOS 热更新解读(一)APatch & JavaScriptCore
之前在手淘实习负责过百川Hotfix能力的建设,Android 端采用阿里自研 Sophix,iOS 端采用 JSPatch,解读 JSPatch 前有必要先了解 Objective-C 与 JavaScript 的互相调用。
iOS 动态更新的几种方案
- WebView 加载 HTML5 动态更新。
- React Native/weex js 动态更新。
- lua 脚本文件控制动态更新(代表框架 WaxPatch )。
- js 脚本文件控制动态更新(代表框架 JSPatch)。
- framework 实现功能模块动态更新。
其中 WaxPatch 和 JSPatch 是使用较广泛的两种热修复方案。而苹果 review guideline 提到只允通过JavaScriptCore.framework
或WebKit
执行脚本,因此 JSPatch 是真正被 Apple 官方支持的。此外鉴于JavaScript
比lua
语言更亲民,使用系统内置的 JavaScriptCore.framework
而无需内嵌lua脚本引擎来解释运行lua代码,JSPatch 便成为目前 iOS 热修复使用最多,效果也最佳的方案。
有关上述几种热修复方案的比较可阅读这两篇文章:
Weex & ReactNative & JSPatch
WaxPatch与JSPatch对比
APatch 与 JSPatch 的关系
JSPatch 使用时需要一个后台下发和管理脚本。阿里百川 HotFix 平台帮助开发者做了这些事。通过提供脚本托管、版本管理、脚本文件及传输过程加密等服务,让开发者无需搭建后台和关心部署操作,只需引入一个 SDK 即可直接使用 JSPatch 进行热修复。这个 SDK 就是 APatch(iOS)。
APatch 在 JSPatch 核心代码的基础上封装了向 HotFix 平台请求脚本/传输解密/脚本管理/本地调试等功能,是配合阿里百川 HotFix 平台一起使用的。
APatch 工作流程
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 —— 基于 JavaScriptCore.framework
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引擎怎么使用。
OC 调用 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
从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
方式二:OC对象实现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"