本篇用于梳理 WKWebView 中 JS 与原生的交互,及 JavaScriptCore 框架在交互过程中起的作用。

WKWebView 中 JS 调用 OC

核心方法:

// 添加 scriptMessageHandler
- (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;

// WKScriptMessageHandler 中对应处理 scriptMessage
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;

使用示例,在初始化 WKWebView 时,初始化 WKWebViewConfiguration,添加对应的 ScriptMessageHandler。实例代码:

WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
configuration.userContentController = [WKUserContentController new];
[configuration.userContentController addScriptMessageHandler:self name:@"btnClick"];

#pragma mark - WKScriptMessageHandler
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
if ([message.name isEqualToString:@"btnClick"]) {
NSDictionary *jsData = message.body;
NSLog(@"%@", message.name, jsData);
// 读取 js function 的字符串
NSString *jsFunctionString = jsData[@"result"];
// 拼接调用该方法的 js 字符串
// convertDictionaryToJson: 方法将 NSDictionary 转成 JSON 格式的字符串
NSString *jsonString = [NSDictionary convertDictionaryToJson:@{@"test":@"123", @"data":@"321"}];
NSString *jsCallBack = [NSString stringWithFormat:@"(%@)(%@);", jsFunctionString, jsonString];
// 执行回调
[self.weWebView evaluateJavaScript:jsCallBack completionHandler:^(id _Nullable result, NSError * _Nullable error) {
if (error) {
NSLog(@"err is %@", error.domain);
}
}];
}
}

注:message 的 body 只能是 NSNumber、NSString、NSDate、NSArray、NSDictionary、NSNull 这几种类型。我们需要在回调环境下,将 js 回调转为 string 后传给原生,执行回调方法。

WKWebView 中 OC 调用 JS

核心方法:

- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler;

OC 调用 JS:

NSString *jsFounction = [NSString stringWithFormat:@"getAppConfig('%@')", APP_CHANNEL_ID];
[self.weWebView evaluateJavaScript:jsFounction completionHandler:^(id object, NSError * _Nullable error) {
NSLog(@"obj:%@---error:%@", object, error);
}];

JavaScriptCore 框架

苹果从 iOS7 开始将 JavaScriptCore 框架引入了 iOS 系统中,成为了系统内置框架,框架名为 JavaScriptCore.framework。

框架结构

苹果官方对 JavaScriptCore 框架的说明:JavaScriptCore

结构上 JavaScriptCore 框架主要分为:

  • JSVirtualMachine
  • JSContext
  • JSValue

JSVirtualMachine

JSVirtualMachine 为 JavaScript 代码的运行提供的一个虚拟机环境。同一时间内,JSVirtualMachine 只能执行一个线程,若想执行对个线程执行任务,需要创建多个 JSVirtualMachine。每个 JSVirtualMachine 都有自己的 GC(垃圾回收器 Garbage Collector),多个 JSVirtualMachine 之间的对象无法传递。

JSVirtualMachine 是一个抽象的 JavaScript 虚拟机,是提供给开发者开发使用的。它的核心是 JavaScriptCore,JavaScriptCore 引擎是一个真实的虚拟机,包含了虚拟机的解释器和运行时部分。解释器用来将高级脚本语言编译成字节码,运行时用来管理运行时的内存空间。

JSContext

JSContext 是 JavaScript 运行环境的上下文,负责原生和 JavaScript 的数据传递。

JSValue

JSValue 是 JavaScript 的值对象。用来记录 JavaScript 的原始值,并提供进行原生值对象转换的接口方法。

三者的关系:

入图可以看到,一个 JSVirtualMachine 里包含了多个 JSContext, 同一个 JSContext 中又可以有多个 JSValue。

JSVirtualMachine、JSContext、JSValue 类提供的接口,可以让原生:

  • 执行 JS 代码
  • 访问 JS 变量
  • 访问和执行 JS 函数
  • 也可以让 JS 调用原生方法

那执行 JavaScript 代码的 JavaScriptCore 和原生应用是怎么交互的?

JSCore 与原生的交互

如图可以看到每个 JSVirtualMachine 对应一个原生的线程,JSVirtualMachine 使用 JSValue 与原生线程通信,遵循 JSExport 协议。这样原生线程可以将类方法和属性提供给 JavaScriptCore 使用,JavaScriptCore 也可以将 JSValue 提供给原生线程使用。

当然 JavaScriptCore 与原生的交互,必须有 JSContext。JSContext 若 init 初始化,默认会使用系统创建的 JSVirtualMachine。若想指定 JSVirtualMachine,需要通过 initWithVirtualMachine 来指定:

// 创建 JSVirtualMachine 对象 jsvm
JSVirtualMachine *jsvm = [[JSVirtualMachine alloc] init];
// 使用 jsvm 的 JSContext 对象 ct
JSContext *ct = [[JSContext alloc] initWithVirtualMachine:jsvm];

原生调用 JS

再看下 JavaScriptCore 在原生代码中调用 JavaScript 的示例:

JSContext *context  = [[JSContext alloc] init];
// 解析执行 JavaScript 脚本
[context evaluateScript:@"var i = 4 + 8"];
// 转换 i 变量为原生对象
NSNumber *number = [context[@"i"] toNumber];
NSLog(@"var i is %@, number is %@", context[@"i"], number);

可以看到 JSContext 通过 evaluateScript: 方法返回 JSValue 对象。苹果官方 JSValue 的相关说明

官方提供了 3 个可以将 JavaScript 对象值类型直接转化为原生类型的接口:

  • toNumber 方法。将 js 值转为 NSNumber 对象
  • toArray 方法。将 js 值转为 NSArray 对象
  • toDictionary 方法。如果变量是 object 类型,可以通过 toDictionary 将 js 值转为 NSDictionary 对象

如若想在原生中使用 JS 的函数方法,可以通过 callWithArguments 方法,传入对应参数调用即可。示例代码:

// 解析执行 JavaScript 脚本
[context evaluateScript:@"function addition(x, y) { return x + y}"];
// 获得 addition 函数
JSValue *addition = context[@"addition"];
// 传入参数执行 addition 函数
JSValue *resultValue = [addition callWithArguments:@[@(4), @(8)]];
// 将 addition 函数执行的结果转成原生 NSNumber 来使用。
NSLog(@"function is %@; reslutValue is %@",addition, [resultValue toNumber]);

简而言之,我们可以通过 evaluateScript 方法,在原生中执行 JS 脚本,并使用 JS 的值对象和函数对象。

那 JavaScript 有如何调用原生的代码?

JS 调用原生

// 在 JSContext 中使用原生 Block 设置一个减法 subtraction 函数
context[@"subtraction"] = ^(int x, int y) {
return x - y;
};

// 在同一个 JSContext 里用 JavaScript 代码来调用原生 subtraction 函数
JSValue *subValue = [context evaluateScript:@"subtraction(4,8);"];
NSLog(@"substraction(4,8) is %@",[subValue toNumber]);

如上代码即完成了一次 JS 对原生的调用。

  • 先在 JSContext 中使用原生 Block 设置一个减法函数
  • 在通过这个 context 用 JavaScript 代码调用原生减法函数

除了使用 Block 的方式,还可以通过 JSExport 协议来实现在 JS 中调用原生代码。即原生代码遵循 JSExport 协议,以提供给 JavaScript 调用。

JSCore 引擎的组成

JavaScriptCore 是一个很复杂的模块,更多的放到后面再深:深入剖析 JavaScriptCore

总结

  • WKWebView 中 JS 调用 OC
  • WKWebView 中 OC 调用 JS
  • JavaScriptCore 框架结构
  • JSVirtualMachine
  • JSContext
  • JSValue
  • JavaScriptCore 与原生的交互
    • OC 调用 JS
    • JS 调用 OC

评论