初识JavaScriptCore

JavaScriptCore介绍

  • OS X Mavericks 和 iOS 7 引入了 JavaScriptCore 库,它把 WebKit 的 JavaScript 引擎用 Objective-C 封装,提供了简单,快速以及安全的方式接入世界上最流行的语言。不管你爱它还是恨它,JavaScript 的普遍存在使得程序员、工具以及融合到 OS X 和 iOS 里这样超快的虚拟机中资源的使用都大幅增长.

  • 在之前的版本,你只能通过向UIWebView发送stringByEvaluatingJavaScriptFromString:消息来执行一段JavaScript脚本,并且如果想用JavaScript调用OC,必须打开一个自定义的URL(例如axe://),然后在webView:shouldStartLoadWithRequest:navigationType:中处理.

  • JavaScriptCore的先进功能

    • 运行JavaScript脚本而不需要依赖UIWebView
    • 使用现代Objective-C的语法(例如Blocks和下标)
    • 在Objective-C和JavaScript之间无缝的传递值或者对象
    • 创建混合对象(原生对象可以将JavaScript值或函数作为一个属性)

JavaScriptCore概述

  • JSValue:代表一个JavaScript实体,一个JSValue可以表示很多JavaScript原始类型例如boolean, integers, doubles,甚至包括对象和函数.
  • JSManagedValue:本质上是一个JSValue,但是可以处理内存管理中的一些特殊情形,它能帮助引用计数和垃圾回收这两种内存管理机制之间进行正常的切换.
  • JSContext:代表JavaScript的运行环境,你需要用JSContext来执行JavaScript代码,所有的JSValue都是捆绑在一个JSContext上的.
  • JSExport:这是一个协议,可以用这个协议将原生对象导出给JavaScript,这样原生对象的属性或者方法就成了JavaScript的属性或者方法,很神奇!
  • JSVirtualMachine:代表一个对象空间,拥有自己的堆结构和垃圾回收机制,大部分情况下不需要和它直接交互,除非要处理一些特殊的多线程或者内存管理问题.

JSContext/JSValue

  • JSContext是运行JavaScript代码的环境,一个JSContext是一个全局环境的实例,如果你写过一个在浏览器内运行的JavaScript, JSContext类似于window,创建一个JSContext后,可以很容易的运行JavaScript代码来创建变量,做计算, 甚至定义方法:
    JSContext *context = [[JSContext alloc] init];
    [context evaluateScript:@"var num = 1 + 2"];
    [context evaluateScript:@"var names = ['Giant', 'Axe', 'GA']"];
    [context evaluateScript:@"var triple = function(value) { return value * 3 }"];
    JSValue *tripleNum = [context evaluateScript:@"triple(num)"];
  • 代码的最后一行,任何出自JSContext的值都被包裹在一个JSValue对象中,像JavaScript这样的动态语言需要一个动态类型,所以JSValue包装了每一个可能的JavaScript值:字符串;数字;数组;对象;方法;甚至错误和特殊的JavaScript值比如null和undefined.
  • JSValue包括了一系列方法用于访问其可能的值以保证有正确的Foundation类型,包括如下:
JavaScript Type JSValue Method Objective-C Type Swift Type
string toString NSString String!
boolean toBool BOOL Bool
number toNumber,toDouble,toInt32,toUInt32 NSNumber,double,int32_t,uint32_t NSNumber!,Double,Int32,UInt32
Date toDate NSDate NSDate!
Array toArray NSArray [AnyObject]!
Object toDictionary NSDictionary [NSObject : AnyObject]!
Object toObject,toObjectOfClass: custom type custom type
  • 从上面的例子中得到tripleNum的值,只需使用适当的方法:
NSLog(@"Tripled: %d", [tripleNum toInt32]);
// Tripled: 9

下标值

  • 对JSContext和JSValue实例使用下标的方式,我们可以很容易的访问我们之前创建的context的任何值. JSContext需要一个字符串下标,而JSValue允许使用字符串或整数标来得到里面的对象和数组:
JSValue *names = context[@"names"];
JSValue *initialName = names[0];
NSLog(@"The first name: %@", [initialName toString]);
// The first name: Giant

调用方法

  • JSValue包装了一个JavaScript函数,我们可以从OC代码中使用Foundation类型作为参数直接调用该函数:
JSValue *tripleFunction = context[@"triple"];
JSValue *result = [tripleFunction callWithArguments:@[@5]];
NSLog(@"Five tripled: %d", [result toInt32]);

错误处理

  • JSContext还有另外一个有用的招数,通过设置上下文的exceptionHandler属性,你可以观察和记录语法,类型以及运行时错误,exceptionHandler是一个接收一个JSContext引用和异常本身的回调处理:
context.exceptionHandler = ^(JSContext *context, JSValue *exception) { 
  NSLog(@"JS Error: %@", exception);
};
[context evaluateScript:@"function multiply(value1, value2) { return value1 * value2 "];
// JS Error: SyntaxError: Unexpected end of script

JavaScript调用

oc调js

  • 例如有一个"Hello.js"文件内容如下:
function printHello() {
}
  • 在Objective-C中调用printHello方法:
    // 取出js路径
    NSString *scriptPath = [[NSBundle mainBundle] pathForResource:@"hello" ofType:@"js"];
    
    // UTF8编码
    NSString *scriptString = [NSString stringWithContentsOfFile:scriptPath encoding:NSUTF8StringEncoding error:nil];
    
    // 初始化JSContext
    JSContext *context = [[JSContext alloc] init];
    
    // 执行JavaScript脚本
    [context evaluateScript:scriptString];
    
    // 取出printHello函数,保存到JSValue中
    JSValue *function = self.context[@"printHello"];
    
    // 调用(如果JSValue是一个js函数,可以用callWithArguments来调用,参数是一个数组,如果没有参数则传入空数组@[])
    [function callWithArguments:@[]];

js调oc

JS调用OC有两个方法:block和JSExport protocol。
  • Block方法:
    // 初始化JSContext
    self.context = [[JSContext alloc] init];
    
    // 定义block保存到context中
    self.context[@"add"] = ^(NSInteger a, NSInteger b) {
        NSLog(@"addNum : %@", @(a + b));
    };
    
    // 执行javaScript
    [self.context evaluateScript:@"add(2,3)"];
  • ** JSExport**方法:
    • 新建一个类,遵守一个我们自定义的继承自JSExport的协议:
    • 然后我们在VC里测试.
#import 
#import 

@protocol JSTestDelegate 

// 测试无参数
- (void)testNoPara;
// 测试一个参数
- (void)testOnePara:(NSString *)msg;
// 测试两个参数
- (void)testTwoPara:(NSString *)msg1 secondPara:(NSString *)msg2;

@end

@interface testJSObject : NSObject 

@end
#import "testJSObject.h"

@implementation testJSObject

- (void)testNoPara
{
    NSLog(@"no para");
}

- (void)testOnePara:(NSString *)msg
{
    NSLog(@"one para");
}

- (void)testTwoPara:(NSString *)msg1 secondPara:(NSString *)msg2
{
    NSLog(@"two para");
}

@end

    // 创建JSContext
    self.context = [[JSContext alloc] init];

    //设置异常处理 
    self.context.exceptionHandler = ^(JSContext *context,JSValue *exception) {
        [JSContext currentContext].exception = exception; 
        NSLog(@"exception:%@",exception);                 
    };
    
    // 将testObj添加到context中
    testJSObject *testObj = [testJSObject new];
    self.context[@"testObject"] = testObj;
   
    NSString *jsStr1 = @"testObject.testNoPara()";
    NSString *jsStr2 = @"testObject.testOnePara()";
    [self.context evaluateScript:jsStr1];
    [self.context evaluateScript:jsStr2];
  • demo比较简单,控制台输出结果如下:
打印结果
  • 唯一要注意的是OC的函数命名和JS函数命名规则问题,协议中定义的testNoPara,testOnePara:,testTwoPara:secondPara:js调用时要注意.

内存管理陷阱

  • Objective-C的内存管理机制是引用计数,JavaScript的内存管理机制是垃圾回收。在大部分情况下,JavaScriptCore能做到在这两种内存管理机制之间无缝无错转换,但也有少数情况需要特别注意.

在block内捕获JSContext

  • Block会为默认为所有被它捕获的对象创建一个强引用。JSContext为它管理的所有JSValue也都拥有一个强引用。并且,JSValue会为它保存的值和它所在的Context都维持一个强引用。这样JSContext和JSValue看上去是循环引用的,然而并不会,垃圾回收机制会打破这个循环引用。看如下例子:
    self.context[@"getVersion"] = ^{
        NSString *versionString = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
        
        versionString = [@"version " stringByAppendingString:versionString];
        
        JSContext *context = [JSContext currentContext]; // 这里不要用self.context
        JSValue *version = [JSValue valueWithObject:versionString inContext:context];
        
        return version;
    };
  • 使用[JSContext currentContext]而不是self.context来在block中使用JSContext,来防止循环引用。

JSManagedValue

  • 当把一个JavaScript值保存到一个本地实例变量上时,需要尤其注意内存管理陷阱。 用实例变量保存一个JSValue非常容易引起循环引用。
  • 看以下下例子,自定义一个UIAlertView,当点击按钮时调用一个JavaScript函数:
#import 
#import 

@interface MyAlertView : UIAlertView

- (id)initWithTitle:(NSString *)title
            message:(NSString *)message
            success:(JSValue *)successHandler
            failure:(JSValue *)failureHandler
            context:(JSContext *)context;

@end
  • 按照一般自定义AlertView的实现方法,MyAlertView需要持有successHandlerfailureHandle这两个JSValue对象

  • 向JavaScript环境注入一个function:

    self.context[@"presentNativeAlert"] = ^(NSString *title,
                                            NSString *message,
                                            JSValue *success,
                                            JSValue *failure) {
        JSContext *context = [JSContext currentContext];
        MyAlertView *alertView = [[MyAlertView alloc] initWithTitle:title
                                                            message:message
                                                            success:success
                                                            failure:failure
                                                            context:context];
        [alertView show];
    };
  • 因为JavaScript环境中都是“强引用”(相对Objective-C的概念来说)的,这时JSContext强引用了一个presentNativeAlert函数,这个函数中又强引用了MyAlertView 等于说JSContext强引用了MyAlertView,而MyAlertView为了持有两个回调强引用了successHandlerfailureHandler这两个JSValue,这样MyAlertView和JavaScript环境互相引用了。

  • 所以苹果提供了一个JSManagedValue类来解决这个问题。

  • 看MyAlertView.m的正确实现:

#import "MyAlertView.h"

@interface MyAlertView() 
@property (strong, nonatomic) JSContext *ctxt;
@property (strong, nonatomic) JSMagagedValue *successHandler;
@property (strong, nonatomic) JSMagagedValue *failureHandler;
@end

@implementation MyAlertView

- (id)initWithTitle:(NSString *)title
            message:(NSString *)message
            success:(JSValue *)successHandler
            failure:(JSValue *)failureHandler
            context:(JSContext *)context {
    
    self = [super initWithTitle:title
                        message:message
                       delegate:self
              cancelButtonTitle:@"No"
              otherButtonTitles:@"Yes", nil];
    
    if (self) {
        _ctxt = context;
        
        _successHandler = [JSManagedValue managedValueWithValue:successHandler];
        // A JSManagedValue by itself is a weak reference. You convert it into a conditionally retained
        // reference, by inserting it to the JSVirtualMachine using addManagedReference:withOwner:
        [context.virtualMachine addManagedReference:_successHandler withOwner:self];
        
        _failureHandler = [JSManagedValue managedValueWithValue:failureHandler];
        [context.virtualMachine addManagedReference:_failureHandler withOwner:self];
    }
    return self;
}

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
    if (buttonIndex == self.cancelButtonIndex) {
        JSValue *function = [self.failureHandler value];
        [function callWithArguments:@[]];
    } else {
        JSValue *function = [self.successHandler value];
        [function callWithArguments:@[]];
    }
    
    [self.ctxt.virtualMachine removeManagedReference:_failureHandler withOwner:self];
    [self.ctxt.virtualMachine removeManagedReference:_successHandler withOwner:self];
}    

@end
  • 分析上面例子,从外部传入的JSValue对象在类内部使用JSManagedValue来保存.
  • JSManagedValue本身是一个弱引用对象,需要调用JSVirtualMachine的addManagedReference:withOwner:
    把它添加到JSVirtualMachine对象中,确保使用过程中JSValue不会被释放.
  • 当用户点击AlertView上的按钮时,根据用户点击哪一个按钮,来执行对应的处理函数,这时AlertView也随即被销毁.这时需要手动调用removeManagedReference:withOwner:
    来移除JSManagedValue.

参考资料

  • 《iOS 7 by tutorials》
  • https://developer.apple.com/videos/play/wwdc2013-615/
  • http://nshipster.com/javascriptcore/

你可能感兴趣的:(初识JavaScriptCore)