澳门新葡萄京官网注册 3

JS与OC的交互01 – JavaScriptCore

本文作者Nate
Cook是一位独立的Web及移动应用开发者,是继Mattt大神之后NSHipster的主要维护者,也是非常知名活跃的Swift博主,并且还是支持自动生成Swift在线文档的SwiftDoc.org网站创造者。在本文中,他介绍了在Swift中使用JavaScript的方法和技巧,对于iOS和Web应用工程师有着非常实用的价值,以下为译文:

一、介绍

JavaScriptCore是webkit的一个重要组成部分,主要是对JS进行解析和提供执行环境。代码是开源的,可以下下来看看(源码)。
iOS7后苹果在iPhone平台推出,极大的方便了我们对js的操作。我们可以脱离webview直接运行我们的js。
iOS7以前我们对JS的操作只有webview里面一个函数stringByEvaluatingJavaScriptFromString,JS对OC的回调都是基于URL的拦截进行的操作。
JavaScriptCore和我们相关的类不是很多,使用起来也非常简单。

‘#import “JSContext.h”
‘#import “JSValue.h”
‘#import “JSManagedValue.h”
‘#import “JSVirtualMachine.h”
‘#import “JSExport.h”

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

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

  • JavaScriptCore的先进功能

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

在RedMonk发布的2015年1月编程语言排行榜中,Swift采纳率排名迅速飙升,从刚刚面世时的68位跃至22位,Objective-C仍然稳居TOP
10,而JavaScript则凭借着其在iOS平台上原生体验优势成为了年度最火热的编程语言。

二、介绍相关类

澳门新葡萄京官网注册 1

一)、JSContext

1、JSContext 是运行 JavaScript 代码的上下文环境。
2、同时也通过JSVirtualMachine管理着所有对象的生命周期,每个JSValue都和JSContext相关联并且强引用context(对于后面的理解循环引用有助)。。
3、一个 JSContext 是一个全局环境的实例,如果你写过一个在浏览器内运行的
JavaScript,JSContext 类似于 window。

获取上下文(VIP核心代码):

// 获取上下文
self.context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

4、创建一个 JSContext 后,可以很容易地 运行 JavaScript 代码
创建变量做计算、甚至 定义方法

evaluateScript:: 运行JS代码

澳门新葡萄京官网注册,例如:

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

而早在2013年苹果发布的OS X Mavericks和iOS
7两大系统中便均已加入了JavaScriptCore框架,能够让开发者轻松、快捷、安全地使用JavaScript语言编写应用。不论叫好叫骂,JavaScript霸主地位已成事实。开发者们趋之若鹜,JS工具资源层出不穷,用于OS
X和iOS系统等高速虚拟机也蓬勃发展起来。

二)、JSValue

1、JS对象在JSVirtualMachine中的一个强引用,其实就是Hybird对象。我们对JS的操作都是通过它。
2、JavaScript和Objective-C数据和方法的桥梁
3、任何出自 JSContext 的值都被包裹在一个 JSValue
对象
中,(可上下自寻例子)。
4、像 JavaScript 这样的动态语言需要一个动态类型,所以 JSValue
包装了每一个可能的 JavaScript
值:字符串和数字数组对象方法;甚至错误和特殊的
JavaScript 值诸如 null 和 undefined。
5、JSValue 包括一系列方法用于访问其可能的值以保证有正确的 Foundation
类型,包括:

澳门新葡萄京官网注册 2

例如:

JSValue *tripleNum = [context evaluateScript:@"triple(num)"];
NSLog(@"Tripled: %d", [tripleNum toInt32]);  // OC 中取值

JSContext/JSValue

三)、JSManagedValue

JS和OC对象的内存管理辅助对象。由于JS内存管理是垃圾回收,并且JS中的对象都是强引用,而OC是引用计数。如果双方相互引用,势必会造成循环引用,而导致内存泄露。我们可以用JSManagedValue保存JSValue来避免。
(下面会专门讲)

  • JSContext是运行JavaScript代码的环境,一个JSContext是一个全局环境的实例,如果你写过一个在浏览器内运行的JavaScript,
    JSContext类似于window,创建一个JSContext后,可以很容易的运行JavaScript代码来创建变量,做计算,
    甚至定义方法:

JSContext即JavaScript代码的运行环境。一个Context就是一个JavaScript代码执行的环境,也叫作用域。当在浏览器中运行JavaScript代码时,JSContext就相当于一个窗口,能轻松执行创建变量、运算乃至定义函数等的JavaScript代码:

四)、JSVirtualMachine

JS运行的虚拟机,有独立的堆空间和垃圾回收机制。

//Objective-C
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)"];

//Swift
let context = JSContext()
context.evaluateScript("var num = 5 + 5")
context.evaluateScript("var names = ['Grace', 'Ada', 'Margaret']")
context.evaluateScript("var triple = function(value) { return value * 3 }")
let tripleNum: JSValue = context.evaluateScript("triple(num)")
五)、JSExport

1、协议,如果采用自定义协议的方法交互,自己定义的协议必须遵守此JSExport协议
2、如果JS对象想直接调用OC对象里面的方法和属性,那么这个OC对象只要实现这个JSExport协议就可以了。
3、该协议规定哪些属性、方法在 JavaScript 中可用。(有点啰嗦……)

 JSContext *context = [[JSContext alloc] init]; [context evaluateScript:@"var num = 1 + 2"]; [context evaluateScript:@"var names = ['Giant', 'Axe', 'GA']"]; [context evaluateScript:@"var triple = function { return value * 3 }"]; JSValue *tripleNum = [context evaluateScript:@"triple"];

像JavaScript这类动态语言需要一个动态类型(Dynamic Type),
所以正如代码最后一行所示,JSContext里不同的值均封装在JSValue对象中,包括字符串、数值、数组、函数等,甚至还有Error以及null和undefined。

三、其他概念

  • 代码的最后一行,任何出自JSContext的值都被包裹在一个JSValue对象中,像JavaScript这样的动态语言需要一个动态类型,所以JSValue包装了每一个可能的JavaScript值:字符串;数字;数组;对象;方法;甚至错误和特殊的JavaScript值比如null和undefined.
  • JSValue包括了一系列方法用于访问其可能的值以保证有正确的Foundation类型,包括如下:

JSValue包含了一系列用于获取Underlying Value的方法,如下表所示:

一)、下标值

1、对 JSContext 和 JSValue
实例使用下标的方式我们可以很容易地访问我们之前创建的 context
的任何值
。JSContext
需要一个字符串下标,或者:objectForKeyedSubscript()
例如:

[context evaluateScript:@"var names = ['Grace', 'Ada', 'Margaret']"];
JSValue *names = context[@"names"];

2、而 JSValue 允许使用字符串或整数标来得到里面的对象和数组:

JSValue *names = context[@"names"];
JSValue *initialName = names[0];
NSLog(@"The first name: %@", [initialName toString]);
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
JavaScript Type
JSValue method
Objective-C Type
Swift Type
string toString NSString String!
boolean toBool BOOL Bool
number toNumbertoDoubletoInt32

toUInt32

NSNumberdoubleint32_t

uint32_t

NSNumber!DoubleInt32

UInt32

Date toDate NSDate NSDate!
Array toArray NSArray [AnyObject]!
Object toDictionary NSDictionary [NSObject : AnyObject]!
Object toObjecttoObjectOfClass: custom type custom type
二)、错误处理

JSContext 还有另外一个有用的招数:通过设置上下文的 exceptionHandler
属性,你可以观察和记录语法,类型以及运行时错误。 exceptionHandler
是一个接收一个 JSContext
引用和异常本身的回调处理:(下面出现context执行js的错误,依然会有错误回调信息)
例如:下面的context执行的js语句里,少了一个 ‘ } ‘,
上面的exceptionHandler就会回调错误信息

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
  • 从上面的例子中得到tripleNum的值,只需使用适当的方法:

想要检索上述示例中的tripleNum值,只需使用相应的方法即可:

四、OC 调用 JavaScriptCore

如果 JSValue 包装了一个 JavaScript 函数,我们可以从 Objective-C / Swift
代码中使用 Foundation 类型作为参数来直接调用该函数。再次,JavaScriptCore
很轻松的处理了这个桥接:

[context evaluateScript:@"var triple = function(value) { return value * 3 }"];   //定义方法
JSValue *tripleFunction = context[@"triple"];                 // 获取JSValue(里面包装了一个方法)
JSValue *result = [tripleFunction callWithArguments:@[@5]];   // 直接调用了JS的方法
NSLog(@"Five tripled: %d", [result toInt32]);

//Objective-C
NSLog(@"Tripled: %d", [tripleNum toInt32]);
// Tripled: 30

//Swift
println("Tripled: (tripleNum.toInt32())")
// Tripled: 30

五、JavaScriptCore 调 OC

现在我们知道了如何从 JavaScript
环境中提取值以及如何调用其中定义的函数。那么反向呢?我们怎样才能从
JavaScript 访问我们在 Objective-C 或 Swift 定义的对象和方法?

让 JSContext 访问我们的本地客户端代码的方式主要有两种:JSExport 协议
Block

NSLog(@"Tripled: %d", [tripleNum toInt32]);// Tripled: 9

下标值(Subscripting Values)

一)、JSExport 协议

第一步:定义协议.并让自定义对象遵守

#pragma mark -
#pragma mark - JS 代理
@protocol ViewControllerDelegate <JSExport>

- (void)callme:(NSString *)string;

@end

第二步:把对象添加到context中,并与Native关联起来

self.context[@"Native"] = self;

第三步:JS中点击调方法

<input type="button" value="点击JS的按钮" onclick="Native.callme('OC?OC?这是JS的函数 你能收到吗??')">

第四步:执行OC方法

#pragma mark -
#pragma mark - JS调 OC 的代码
- (void)callme:(NSString *)string {
    NSLog(@"callme:%@",string);
}

下标值

  • 对JSContext和JSValue实例使用下标的方式,我们可以很容易的访问我们之前创建的context的任何值.
    JSContext需要一个字符串下标,而JSValue允许使用字符串或整数标来得到里面的对象和数组:

JSValue *names = context[@"names"];JSValue *initialName = names[0];NSLog(@"The first name: %@", [initialName toString]);// The first name: Giant

通过在JSContext和JSValue实例中使用下标符号可以轻松获取上下文环境中已存在的值。其中,JSContext放入对象和数组的只能是字符串下标,而JSValue则可以是字符串或整数下标。

二)、Block

第一步:JS代码

<input type="button" value="点击JS的按钮,通过Block 传值" onclick="deliverValue('OC?OC?这是JS的Block 你能收到吗??')">

第二步:OC代码

   self.context[@"deliverValue"] = ^(NSString *jsString){

        NSLog(@"JSBLOCK = %@",jsString);

    };

调用方法

  • JSValue包装了一个JavaScript函数,我们可以从OC代码中使用Foundation类型作为参数直接调用该函数:

JSValue *tripleFunction = context[@"triple"];JSValue *result = [tripleFunction callWithArguments:@[@5]];NSLog(@"Five tripled: %d", [result toInt32]);
//Objective-C
JSValue *names = context[@"names"];
JSValue *initialName = names[0];
NSLog(@"The first name: %@", [initialName toString]);
// The first name: Grace

//Swift
let names = context.objectForKeyedSubscript("names")
let initialName = names.objectAtIndexedSubscript(0)
println("The first name: (initialName.toString())")
// The first name: Grace
三)、内存管理

现在来说说内存管理的注意点,OC使用的ARC,JS使用的是垃圾回收机制,并且所有的引用是都强引用,不过JS的循环引用,垃圾回收会帮它们打破。JavaScriptCore里面提供的API,正常情况下,OC和JS对象之间内存管理都无需我们去关心。不过还是有几个注意点需要我们去留意下。
1、不要在block里面直接使用context,或者使用外部的JSValue对象。

//错误代码:使用了context
self.context[@"block"] = ^(){
     JSValue *value = [JSValue valueWithObject:@"aaa" inContext:self.context];
};

这个代码,不用自己看了,编译器都会提示你的。这个block里面使用self,很容易就看出来了。

 //一个比较隐蔽的 错误代码 使用了value
     JSValue *value = [JSValue valueWithObject:@"ssss" inContext:self.context];
    self.context[@"log"] = ^(){
        NSLog(@"%@",value);
    };

这个是block里面使用了外部的value,value对context和它管理的JS对象都是强引用。这个value被block所捕获,这边同样也会内存泄露,context是销毁不掉的。

//正确的做法,str对象是JS那边传递过来。
self.context[@"log"] = ^(NSString *str){
        NSLog(@"%@",str);
    };

2、OC对象不要用属性直接保存JSValue对象,因为这样太容易循环引用了。
看个demo,把上面的示例改下:

//定义一个JSExport protocol
@protocol JSExportTest <JSExport>
//用来保存JS的对象
@property (nonatomic, strong) JSvalue *jsValue;
@end
//建一个对象去实现这个协议:
@interface JSProtocolObj : NSObject<JSExportTest>
@end
@implementation JSProtocolObj
@synthesize jsValue = _jsValue;
@end
//在VC中进行测试
@interface ViewController () <JSExportTest>
@property (nonatomic, strong) JSProtocolObj *obj;
@property (nonatomic, strong) JSContext *context;
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    //创建context
    self.context = [[JSContext alloc] init];
    //设置异常处理
    self.context.exceptionHandler = ^(JSContext *context, JSValue *exception) {
        [JSContext currentContext].exception = exception;
        NSLog(@"exception:%@",exception);
    };
   //加载JS代码到context中
   [self.context evaluateScript:
   @"function callback (){};
    function setObj(obj) {
    this.obj = obj;               // JS保存了obj
    obj.jsValue=callback;         // obj保留了JS
}"];
   //调用JS方法
   [self.context[@"setObj"] callWithArguments:@[self.obj]];  
}

上面的例子很简单,调用JS方法,进行赋值,JS对象保留了传进来的obj,最后,JS将自己的回调callback赋值给了obj,方便obj下次回调给JS;由于JS那边保存了obj,而且obj这边也保留了JS的回调。这样就形成了循环引用。

怎么解决这个问题?我们只需要打破obj对JSValue对象的引用即可。当然,不是我们OC中的weak。而是之前说的内存管理辅助对象JSManagedValue。

JSManagedValue 本身就是我们需要的弱引用。用官方的话来说叫garbage
collection weak
reference。但是它帮助我们持有JSValue对象必须同时满足一下两个条件(不翻译了,翻译了怪怪的!):

The JSManagedValue's JavaScript value is reachable from JavaScript
The owner of the managed reference is reachable in Objective-C. Manually adding or removing the managed reference in the JSVirtualMachine determines reachability.

意思很简单,JSManagedValue
帮助我们保存JSValue
,那里面保存的JS对象必须在JS中存在,同时
JSManagedValue 的owner在OC中也存在。我们可以通过它提供的两个方法

//通过JSManagedValue的方法
+ (JSManagedValue )managedValueWithValue:(JSValue )value;
+ (JSManagedValue )managedValueWithValue:(JSValue )value andOwner:(id)owner  //创建JSManagedValue对象。

//通过JSVirtualMachine的方法
- (void)addManagedReference:(id)object withOwner:(id)owner //来建立这个弱引用关系。
- (void)removeManagedReference:(id)object withOwner:(id)owner //来手动移除他们之间的联系。

把刚刚的代码改下:

//定义一个JSExport protocol
@protocol JSExportTest <JSExport>
//用来保存JS的对象
@property (nonatomic, strong) JSValue *jsValue;
@end
//建一个对象去实现这个协议:
@interface JSProtocolObj : NSObject<JSExportTest>

//添加一个JSManagedValue用来保存JSValue
// 改的地方1
@property (nonatomic, strong) JSManagedValue *managedValue;
@end
@implementation JSProtocolObj
@synthesize jsValue = _jsValue;
//重写setter方法
// 改的地方2
- (void)setJsValue:(JSValue *)jsValue
{
    _managedValue = [JSManagedValue managedValueWithValue:jsValue];
    [[[JSContext currentContext] virtualMachine] addManagedReference:_managedValue 
    withOwner:self];
}
@end
//在VC中进行测试
@interface ViewController () <JSExportTest>
@property (nonatomic, strong) JSProtocolObj *obj;
@property (nonatomic, strong) JSContext *context;
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    //创建context
    self.context = [[JSContext alloc] init];
    //设置异常处理
    self.context.exceptionHandler = ^(JSContext *context, JSValue *exception) {
        [JSContext currentContext].exception = exception;
        NSLog(@"exception:%@",exception);
    };
   //加载JS代码到context中
   [self.context evaluateScript:
   @"function callback (){}; 
   function setObj(obj) {
   this.obj = obj;
   obj.jsValue=callback;
   }"];
   //调用JS方法
   [self.context[@"setObj"] callWithArguments:@[self.obj]];  
}

注:以上代码只是为了突出用 JSManagedValue来保存 JSValue,所以重写了
setter
方法。实际不会写这么搓的姿势。。。应该根据回调方法传进来参数,进行保存
JSValue。

Demo链接:吻我

错误处理

  • 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
  • 例如有一个”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:@[]];

而Swift语言毕竟才诞生不久,所以并不能像Objective-C那样自如地运用下标符号,目前,Swift的方法仅能实现objectAtKeyedSubscript()和objectAtIndexedSubscript()等下标。

js调oc

函数调用(Calling Functions)

JS调用OC有两个方法:block和JSExport protocol。
  • Block方法:

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

#import <Foundation/Foundation.h>#import <JavaScriptCore/JavaScriptCore.h>@protocol JSTestDelegate <JSExport>// 测试无参数- testNoPara;// 测试一个参数- testOnePara:(NSString *)msg;// 测试两个参数- testTwoPara:(NSString *)msg1 secondPara:(NSString *)msg2;@end@interface testJSObject : NSObject <JSTestDelegate>@end

#import "testJSObject.h"@implementation testJSObject- testNoPara{ NSLog(@"no para");}- testOnePara:(NSString *)msg{ NSLog(@"one para");}- 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比较简单,控制台输出结果如下:

澳门新葡萄京官网注册 3打印结果

  • 唯一要注意的是OC的函数命名和JS函数命名规则问题,协议中定义的testNoPara,testOnePara:,testTwoPara:secondPara:js调用时要注意.

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

  • 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,来防止循环引用。

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

#import <UIKit/UIKit.h>#import <JavaScriptCore/JavaScriptCore.h>@interface MyAlertView : UIAlertView- 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() <UIAlertViewDelegate>@property (strong, nonatomic) JSContext *ctxt;@property (strong, nonatomic) JSMagagedValue *successHandler;@property (strong, nonatomic) JSMagagedValue *failureHandler;@end@implementation MyAlertView- 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  { _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;}- 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》

我们可以将Foundation类作为参数,从Objective-C/Swift代码上直接调用封装在JSValue的JavaScript函数。这里,JavaScriptCore再次发挥了衔接作用。

//Objective-C
JSValue *tripleFunction = context[@"triple"];
JSValue *result = [tripleFunction callWithArguments:@[@5] ];
NSLog(@"Five tripled: %d", [result toInt32]);

//Swift
let tripleFunction = context.objectForKeyedSubscript("triple")
let result = tripleFunction.callWithArguments([5])
println("Five tripled: (result.toInt32())")

异常处理(Exception Handling)

JSContext还有一个独门绝技,就是通过设定上下文环境中exceptionHandler的属性,可以检查和记录语法、类型以及出现的运行时错误。exceptionHandler是一个回调处理程序,主要接收JSContext的reference,进行异常情况处理。

//Objective-C
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

//Swift
context.exceptionHandler = { context, exception in
    println("JS Error: (exception)")
}
context.evaluateScript("function multiply(value1, value2) { return value1 * value2 ")
// JS Error: SyntaxError: Unexpected end of script

JavaScript函数调用

了解了从JavaScript环境中获取不同值以及调用函数的方法,那么反过来,如何在JavaScript环境中获取Objective-C或者Swift定义的自定义对象和方法呢?要从JSContext中获取本地客户端代码,主要有两种途径,分别为Blocks和JSExport协议。

  • Blocks (块)

在JSContext中,如果Objective-C代码块赋值为一个标识符,JavaScriptCore就会自动将其封装在JavaScript函数中,因而在JavaScript上使用Foundation和Cocoa类就更方便些——这再次验证了JavaScriptCore强大的衔接作用。现在CFStringTransform也能在JavaScript上使用了,如下所示:

//Objective-C
context[@"simplifyString"] = ^(NSString *input) {
   NSMutableString *mutableString = [input mutableCopy];
   CFStringTransform((__bridge CFMutableStringRef)mutableString, NULL, kCFStringTransformToLatin, NO);
   CFStringTransform((__bridge CFMutableStringRef)mutableString, NULL, kCFStringTransformStripCombiningMarks, NO);
   return mutableString;
};
NSLog(@"%@", [context evaluateScript:@"simplifyString('안녕하새요!')"]);

//Swift
let simplifyString: @objc_block String -> String = { input in
    var mutableString = NSMutableString(string: input) as CFMutableStringRef
    CFStringTransform(mutableString, nil, kCFStringTransformToLatin, Boolean(0))
    CFStringTransform(mutableString, nil, kCFStringTransformStripCombiningMarks, Boolean(0))
    return mutableString
}
context.setObject(unsafeBitCast(simplifyString, AnyObject.self), forKeyedSubscript: "simplifyString")

println(context.evaluateScript("simplifyString('안녕하새요!')"))
// annyeonghasaeyo!

需要注意的是,Swift的speedbump只适用于Objective-C block,对Swift闭包无用。要在一个JSContext里使用闭包,有两个步骤:一是用@objc_block来声明,二是将Swift的knuckle-whitening
unsafeBitCast()函数转换为 AnyObject。

  • 内存管理(Memory
    Management)

代码块可以捕获变量引用,而JSContext所有变量的强引用都保留在JSContext中,所以要注意避免循环强引用问题。另外,也不要在代码块中捕获JSContext或任何JSValues,建议使用[JSContext
currentContext]来获取当前的Context对象,根据具体需求将值当做参数传入block中。

  • JSExport协议

借助JSExport协议也可以在JavaScript上使用自定义对象。在JSExport协议中声明的实例方法、类方法,不论属性,都能自动与JavaScrip交互。文章稍后将介绍具体的实践过程。

JavaScriptCore实践

我们可以通过一些例子更好地了解上述技巧的使用方法。先定义一个遵循JSExport子协议PersonJSExport的Person
model,再用JavaScript在JSON中创建和填入实例。有整个JVM,还要NSJSONSerialization干什么?

  • PersonJSExports和Person

Person类执行的PersonJSExports协议具体规定了可用的JavaScript属性。,在创建时,类方法必不可少,因为JavaScriptCore并不适用于初始化转换,我们不能像对待原生的JavaScript类型那样使用var
person = new Person()。

//Objective-C
// in Person.h -----------------
@class Person;
@protocol PersonJSExports <JSExport>
    @property (nonatomic, copy) NSString *firstName;
    @property (nonatomic, copy) NSString *lastName;
    @property NSInteger ageToday;
    - (NSString *)getFullName;
    // create and return a new Person instance with `firstName` and `lastName`
    + (instancetype)createWithFirstName:(NSString *)firstName lastName:(NSString *)lastName;
@end
@interface Person : NSObject <PersonJSExports>
    @property (nonatomic, copy) NSString *firstName;
    @property (nonatomic, copy) NSString *lastName;
    @property NSInteger ageToday;
@end
// in Person.m -----------------
@implementation Person
- (NSString *)getFullName {
    return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
}
+ (instancetype) createWithFirstName:(NSString *)firstName lastName:(NSString *)lastName {
    Person *person = [[Person alloc] init];
    person.firstName = firstName;
    person.lastName = lastName;
    return person;
}
@end

//Swift
// Custom protocol must be declared with `@objc`
@objc protocol PersonJSExports : JSExport {
    var firstName: String { get set }
    var lastName: String { get set }
    var birthYear: NSNumber? { get set }
    func getFullName() -> String
    /// create and return a new Person instance with `firstName` and `lastName`
    class func createWithFirstName(firstName: String, lastName: String) -> Person
}
// Custom class must inherit from `NSObject`
@objc class Person : NSObject, PersonJSExports {
    // properties must be declared as `dynamic`
    dynamic var firstName: String
    dynamic var lastName: String
    dynamic var birthYear: NSNumber?
    init(firstName: String, lastName: String) {
        self.firstName = firstName
        self.lastName = lastName
    }
    class func createWithFirstName(firstName: String, lastName: String) -> Person {
        return Person(firstName: firstName, lastName: lastName)
    }
    func getFullName() -> String {
        return "(firstName) (lastName)"
    }
}
  • 配置JSContext

创建Person类之后,需要先将其导出到JavaScript环境中去,同时还需导入Mustache
JS库,以便对Person对象应用模板。

//Objective-C
// export Person class
context[@"Person"] = [Person class];
// load Mustache.js
NSString *mustacheJSString = [NSString stringWithContentsOfFile:... encoding:NSUTF8StringEncoding error:nil];
[context evaluateScript:mustacheJSString];

//Swift
// export Person class
context.setObject(Person.self, forKeyedSubscript: "Person")
// load Mustache.js
if let mustacheJSString = String(contentsOfFile:..., encoding:NSUTF8StringEncoding, error:nil) {
    context.evaluateScript(mustacheJSString)
}
  • JavaScript数据&处理

以下简单列出一个JSON范例,以及用JSON来创建新Person实例。

注意:JavaScriptCore实现了Objective-C/Swift的方法名和JavaScript代码交互。因为JavaScript没有命名好的参数,任何额外的参数名称都采取驼峰命名法(Camel-Case),并附加到函数名称上。在此示例中,Objective-C的方法createWithFirstName:lastName:在JavaScript中则变成了createWithFirstNameLastName()。

//JSON
[
    { "first": "Grace",     "last": "Hopper",   "year": 1906 },
    { "first": "Ada",       "last": "Lovelace", "year": 1815 },
    { "first": "Margaret",  "last": "Hamilton", "year": 1936 }
]

//JavaScript
var loadPeopleFromJSON = function(jsonString) {
    var data = JSON.parse(jsonString);
    var people = [];
    for (i = 0; i < data.length; i++) {
        var person = Person.createWithFirstNameLastName(data[i].first, data[i].last);
        person.birthYear = data[i].year;
        people.push(person);
    }
    return people;
}
  • 动手一试

现在你只需加载JSON数据,并在JSContext中调用,将其解析到Person对象数组中,再用Mustache模板渲染即可:

//Objective-C
// get JSON string
NSString *peopleJSON = [NSString stringWithContentsOfFile:... encoding:NSUTF8StringEncoding error:nil];
// get load function
JSValue *load = context[@"loadPeopleFromJSON"];
// call with JSON and convert to an NSArray
JSValue *loadResult = [load callWithArguments:@[peopleJSON]];
NSArray *people = [loadResult toArray];
// get rendering function and create template
JSValue *mustacheRender = context[@"Mustache"][@"render"];
NSString *template = @"{{getFullName}}, born {{birthYear}}";
// loop through people and render Person object as string
for (Person *person in people) {
   NSLog(@"%@", [mustacheRender callWithArguments:@[template, person]]);
}
// Output:
// Grace Hopper, born 1906
// Ada Lovelace, born 1815
// Margaret Hamilton, born 1936

//Swift
// get JSON string
if let peopleJSON = NSString(contentsOfFile:..., encoding: NSUTF8StringEncoding, error: nil) {
    // get load function
    let load = context.objectForKeyedSubscript("loadPeopleFromJSON")
    // call with JSON and convert to an array of `Person`
    if let people = load.callWithArguments([peopleJSON]).toArray() as? [Person] {

        // get rendering function and create template
        let mustacheRender = context.objectForKeyedSubscript("Mustache").objectForKeyedSubscript("render")
        let template = "{{getFullName}}, born {{birthYear}}"

        // loop through people and render Person object as string
        for person in people {
            println(mustacheRender.callWithArguments([template, person]))
        }
    }
}
// Output:
// Grace Hopper, born 1906
// Ada Lovelace, born 1815
// Margaret Hamilton, born 1936

发表评论

电子邮件地址不会被公开。 必填项已用*标注