浅谈JavaScriptCore

本文由我们团队的王瑞华童鞋撰写。

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 typeJavaScript type
nilundefined
NSNullnull
NSStringstring
NSNumbernumber, boolean
NSDictionaryObject object
NSArrayArray object
NSDateDate 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 <JSExport>
    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 <JSExport , NSObject>
  
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 <JDRWebViewJSExportProtocol>
@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.<name>.postMessage(<messageBody>)。WKWebView 对于JavaScript 的调用只提供了一种方法:

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

目前项目中考虑到 应用 WKWebview 与 UIWebview + JavaScriptCore 调用方式的差异,暂时放弃了对于 WKWebview的支持。

参考

  1. JavaScriptCore
    原文作者:知识小集
    原文地址: https://www.jianshu.com/p/932699e3450e
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞