来源:XcodeMen(王瑞华)
链接:http://t.cn/RVqQI5p
本文由我们团队的王瑞华童鞋撰写。
OS X Mavericks 和 iOS 7 引入了 JavaScriptCore 库,它把 WebKit 的 JavaScript 引擎用 Objective-C 封装,提供了简单,快速以及安全的方式接入世界上最流行的语言。在项目的实际开发中,由于需要与 H5 端有交互,所以采用了JavaScriptCore的交互方式,借此参与该项目的机会,对JavaScriptCore也有了一些简单的了解。
JSContext/JSValue
JSContext 是 JavaScript 的执行环境。所有 JavaScript 执行发生的背景,以及所有 JavaScript 值被绑在这一个上下文中。JSContext 类似于 UIWindow 的概念,所有的执行都在改环境中发生:
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 *tripleNum = [context evaluateScript:@"triple(num)"];
NSLog(@"Tripled: %d", [tripleNum toInt32]);
// Tripled: 30
代码最后一行,每个 JSValue 起源于 JSContext 和 持有它的强引用。当一个 JSValue 实例方法创造一个新的 JSValue时,该新 JSValue 起源于同一个 JSContext。 JSValue 包装了每一个可能的 JavaScript 值:字符串和数字;数组、对象和方法;甚至错误和特殊的 JavaScript 值诸如 null 和 undefined。
OBJECTIVE-C TYPE | JAVASCRIPT TYPE |
---|---|
nil | undefined |
NSNull | null |
NSString | string |
NSNumber | number, boolean |
NSDictionary | Object object |
NSArray | Array object |
NSDate | Date object |
NSBlock (1) | Function object (1) |
id (2) | Wrapper object (2) |
Class (3) | Constructor object (3) |
下标支持
作为下标传递的对象键将被转换为一个 JavaScript 值, 然后值转换为一个用作获取属性名称的字符串。
JSValue *names = context[@"names"];
JSValue *initialName = names[0];
NSLog(@"The first name: %@", [initialName toString]);
// The first name: Grace
该简易方法对应于以下两个方法:
- (JSValue *)objectForKeyedSubscript:(id)key;
- (JSValue *)objectAtIndexedSubscript:(NSUInteger)index;
调用方法
JSValue 包装了一个 JavaScript 函数,我们可以从 Objective-C 代码中使用 Foundation 类型作为参数来直接调用该函数。
JSValue *tripleFunction = context[@"triple"];
JSValue *result = [tripleFunction callWithArguments:@[@5] ];
NSLog(@"Five tripled: %d", [result toInt32]);
JavaScript 调用
上面说明的 OC 调用 JavaScript 的方法。那么接下来,说明 JavaScript 访问 OC 定义的对象和方法。我粗浅的认为就是在遵守该协议后,JavaScript 就可以调用 OC 实现的方法和属性等。
让 JSContext 访问我们的本地客户端代码的方式主要有两种:JSExport 协议和block。
JSExport 协议
JSExport 提供一个将 OC 中的类、实例方法和属性等导出为 JavaScript 函数的方法。
该协议只包含了一个方法:
JSExportAs(PropertyName, Selector)
即当导出到 JavaScript 时,重命名一个 selector。
这就需要熟悉它的做法,当带有一个或多个参数的 seletor 转变为 JavaScript 的属性名字时,在默认情况下一个属性名字将采用以下方式生成:
- 所有的冒号将从 Selector 当中移除掉
- 任何一个后边跟着冒号的小写字母开头的单词将被改换为大写。
在这种默认的转换方式下,一个 selector 如 doFoo:withBar: 将被导为doFooWithBar 。这种默认的改变有可能被 JSExportAs 宏 所改写,例如将 doFoo:withBar: 导出为 doFoo 。实际写法如下:
@protocol MyClassJavaScriptMethods
JSExportAs(doFoo,
- (void)doFoo:(id)foo withBar:(id)bar
);
@end
请注意 JSExport 宏只可能适用于带有一个或更多的参数的selector。
Blocks
当一个 Objective-C block 被赋给 JSContext 里的一个标识符,JavaScriptCore 会自动的把 block 封装在 JavaScript 函数里。如下:
JSContext *context = [[JSContext alloc] init];
context[@"simplifyString"] = ^(NSString *input) {
NSMutableString *mutableString = [input mutableCopy];
CFStringTransform((__bridge CFMutableStringRef)mutableString, NULL, kCFStringTransformToLatin, NO);
return mutableString;
};
NSLog(@"%@", [context evaluateScript:@"simplifyString(‘什么!‘)"]);
// shén me!
JSManagedValue 与内存管理
由于 block 可以保有变量引用,而且 JSContext 也强引用它所有的变量,为了避免强引用循环需要特别小心。OC 采用的是引用计数机制,而 JavaScript采用的垃圾回收机制。基于此项机制,便出现了 JSManagedValue ,它的主要用途是存储一个 JSValue,这样可以避免一个保留环的产生。
JavaScript 与 OC 交互的实际实例
在开发项目中,我们采用了 JavaScript 的方式完成了与 Native 的交互,使得比单纯的运用 UIWebview 的 stringByEvaluatingJavaScriptFromString: 更加高效。
如上所述,让 JSContext 访问我们的本地客户端代码的方式主要有两种:JSExport 协议和block。在我们项目中主要采用了 JSExport 的方式去实现。
1. JDRWebViewJSExportProtocol 和 JDRWebViewBaseHandler
我们的 JDRWebViewBaseHandler 类实现了 JDRWebViewJSExportProtocol 协议,该协议规定了那些方法或者属性可在 JavaScript 中使用。
@protocol JDRWebViewJSExportProtocol
JSExportAs
(closeForJS /**H5调用的 Webview 关闭方法的别名**/,
@optional
- (void)close:(id)JSON callback:(JSValue*) callback
);
JSExportAs
(goBackForJS /**H5调用的 Webview 关闭方法的别名**/,
@optional
- (void)goBack:(id)JSON callback:(JSValue*) callback
);
@end
@interface JDRWebViewBaseHandler : NSObject
@property (nonatomic , weak) JDRWebViewController *webViewController;
-(instancetype)initWithWebViewController:(JDRWebViewController *) webViewController NS_DESIGNATED_INITIALIZER;
@end
@implementation JDRWebViewBaseHandler
-(instancetype)initWithWebViewController:(JDRWebViewController *) webViewController{
if (self = [super init]) {
_webViewController = webViewController;
}
return self;
}
-(void)goBack:(id)JSON callback:(JSValue *)callback {
WEAK_SELF(weakSelf);
dispatch_async(dispatch_get_main_queue(), ^{
STRONG_SELF(strongSelf, weakSelf);
//判断 backPressCallBack 是否已经设置 ,如果已经设置监听方法 , 则执行 监听回退的 JS 方法
if ([strongSelf.webViewController.webView canGoBack]) {
[(UIWebView*)strongSelf.webViewController.webView stopLoading];
[(UIWebView*)strongSelf.webViewController.webView goBack];
}else {
[strongSelf close:nil callback:nil];
}
});
}
-(void)close:(id)JSON callback:(JSValue *)callback {
//目前关闭 WebView 方法不关注 callback 参数
WEAK_SELF(weakSelf);
dispatch_async(dispatch_get_main_queue(), ^{
STRONG_SELF(strongSelf, weakSelf);
if ([strongSelf.webViewController.navigationController popViewControllerAnimated:YES] == nil) {
[strongSelf.webViewController dismissViewControllerAnimated:YES completion:nil];
}
});
}
@end
2. JSContext 配置
然后,我们可以用我们已经创建的 JDRWebViewBaseHandler 类,我们需要将其导出到 JavaScript 环境中。
self.JSHandler = [[JDRWebViewBaseHandler alloc] initWithWebViewController:self];
// 以 JSExport 协议关联 native 的方法 *platform* 是暂时定义的关键字
self.context[@"platform"] = self.JSHandler;
3.在 H5 页面中书写 JavaScript 函数调用 OC
close: function() {
platform.coselForJS();
}
结语
通常对于 JavaScript 与 OC 的交互,会采用 JavaScriptCore 包装一层协议方法,原始一点的方法则是通过截取 UIWebView 的协议实现。随着 iOS 8 之后 WKWebview的出现,又出现了一个新的方式,而对于 WKWebView 来说是不支持直接与 JavaScriptCore 交互的。通常采取 WKUserContentController 采用它的协议方法
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
如果使用 WKWebview 的话,对于 H5 中的 JavaScript 写法又需要改动为 window.webkit.messageHandlers..postMessage()。WKWebView 对于JavaScript 的调用只提供了一种方法:
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler
目前项目中考虑到 应用 WKWebview 与 UIWebview + JavaScriptCore 调用方式的差异,暂时放弃了对于 WKWebview的支持。
参考
- JavaScriptCore
http://nshipster.cn/javascriptcore/