之前在手淘实习负责过百川Hotfix能力的建设,Android 端采用阿里自研 Sophix,iOS 端采用 JSPatch,解读 JSPatch 前有必要先了解 Objective-C 与 JavaScript 的互相调用。

iOS 动态更新的几种方案

其中 WaxPatch 和 JSPatch 是使用较广泛的两种热修复方案。而苹果 review guideline 提到只允通过JavaScriptCore.frameworkWebKit执行脚本,因此 JSPatch 是真正被 Apple 官方支持的。此外鉴于JavaScriptlua语言更亲民,使用系统内置的 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 获取 keyiv
  • 使用 keyiv 进行 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

JSContextJSValue 实例使用下标可以访问之前创建的 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"