JavaScriptCore框架入门

在进入主题之前,先简单了解一下主要所涉及的类

JSContext
  • JavaScript执行环境
JSValue
  • 强引用JavaScript值(这些JavaScript值可以是JavaScript函数、JavaScript变量等)
  • 绑定在一个JSContext上,绑定属于强引用

主题

  1. Objective-C调用JavaScript
  2. JavaScript调用Objective-C
  3. 内存管理
  4. 线程
  5. JavaScriptCore与WebView

1. Objective-C调用JavaScript

Objective-C调用JavaScript函数的基本步骤:

  1. 获取相关的JavaScript代码
  2. 通过JSContextevaluateScript:方法,装载JavaScript代码
  3. 通过JSContextobjectForKeyedSubscript:方法,获取已装载的JavaScript函数,获取到的JavaScript函数也就是一个JSValue对象
  4. 通过JSValuecallWithArguments:方法,调用JavaScript函数

下面通过示例,进行演示。

function.js文件中的内容如下,定义了一个函数,其名称为factorail

1
2
3
4
5
6
7
8
9
var factorail = function(n) {
if (n < 0) {
return;
}
if (n === 0) {
return 1;
}
return n * factorail(n - 1);
}

Objective-C调用JavaScript函数:

1
2
3
4
5
6
7
8
9
10
11
// 获取`function.js`文件中的JavaScript代码
NSString *scriptPath = [[NSBundle mainBundle] pathForResource:@"functions" ofType:@"js"];
NSString *funcScript = [NSString stringWithContentsOfFile:scriptPath encoding:NSUTF8StringEncoding error:nil];
// 创建一个JSContext
JSContext *ctx = [JSContext new];
// 装载JavaScript代码
[ctx evaluateScript:funcScript];
// 获取`factorail`JavaScript函数
JSValue *jsFunctionValue = ctx[@"factorail"];// 等同于[ctx objectForKeyedSubscript:@"factorail"];
// 调用JavaScript函数,获取返回值
JSValue *jsReturnValue = [jsFunctionValue callWithArguments:@[@10]];

2. JavaScript调用Objective-C

有两种方式,可以实现JavaScript调用Objective-C。

  • 2.1. 使用Block,实现JavaScript函数
  • 2.2. 使用JSExport协议,实现JavaScript对象

2.1. 使用Block

具体说,就是通过JSContextsetObject:forKeyedSubscript:方法,为指定的下标设置一个Block,这样就相当于在JSContext中装载了一个名称为下标名的JavaScript函数,这个函数的参数就是Block的参数,返回值类型就是Block的返回值类型。

在使用Block时,有两点需要注意:

  1. 避免捕获JSValue:应将其作为参数传递到Block中
  2. 避免捕获JSContext:应在Block中使用[JSContext currentContext]

示例,在JSContext中装载一个名为makeUIColorWithNSDictionary的JavaScript函数。

1
2
3
4
5
6
7
8
9
10
11
#define RGB_COMPONENT_RED   @"red"
#define RGB_COMPONENT_GREEN @"green"
#define RGB_COMPONENT_BLUE @"blue"

JSContext *ctx = [JSContext new];
ctx[@"makeUIColorWithNSDictionary"] = ^UIColor *(NSDictionary<NSString *, NSNumber *> *rgb) {
float r = [rgb[RGB_COMPONENT_RED] floatValue];
float g = [rgb[RGB_COMPONENT_GREEN] floatValue];
float b = [rgb[RGB_COMPONENT_BLUE] floatValue];
return [UIColor colorWithRed:r green:g blue:b alpha:1.0];
};

装载后,就可以调用该JavaScript函数了,因为该JavaScript函数是通过Objective-C实现的,在其它JavaScript函数中调用该JavaScript函数,就可以实现JavaScript调用Objective-C。

接着,在function.js文件中加入colorWithRGBDictionary(rgb)JavaScript函数,这个函数,调用了上面已经为JSContext设置的makeUIColorWithNSDictionary

1
2
3
4
5
6
function colorWithRGBDictionary(rgb) {
if (rgb) {
return makeUIColorWithNSDictionary(rgb);
}
return undefined;
}

其余的主要代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 获取`function.js`文件中的JavaScript代码
NSString *scriptPath = [[NSBundle mainBundle] pathForResource:@"functions" ofType:@"js"];
NSString *funcScript = [NSString stringWithContentsOfFile:scriptPath encoding:NSUTF8StringEncoding error:nil];
// 装载JavaScript代码
[ctx evaluateScript:funcScript];
// 获取`function.js`文件中的`colorWithRGBDictionary`函数
JSValue *jsFunctionValue = ctx[@"colorWithRGBDictionary"];
// 执行`colorWithRGBDictionary`函数
NSDictionary *rgb = @{
RGB_COMPONENT_RED: @(0.3),
RGB_COMPONENT_GREEN: @(0.5),
RGB_COMPONENT_BLUE: @(0.8),
};
JSValue *colorValue = [jsFunctionValue callWithArguments:@[rgb]];
UIColor *color = [colorValue toObject];

2.2. 使用JSExport协议

该方式的优点:很容易让JavaScript与Objective-C对象进行交互。

JSExport协议的作用就是将Objective-C类、实例方法、类方法及属性暴露给JavaScript代码。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@class MyPoint;

@protocol MyPointExport <NSObject, JSExport>
@property (nonatomic, assign) double x;
@property (nonatomic, assign) double y;

+ (MyPoint *)makePointWithX:(double)x y:(double)y;
@end

@interface MyPoint : NSObject <MyPointExport>
@property (nonatomic, assign) double x;
@property (nonatomic, assign) double y;

+ (MyPoint *)makePointWithX:(double)x y:(double)y;
- (double)distanceBetweenOriginalPoint;
@end

@implementation MyPoint

+ (MyPoint *)makePointWithX:(double)x y:(double)y {
MyPoint *p = [MyPoint new];
p.x = x;
p.y = y;
return p;
}

- (NSString *)description {
return [NSString stringWithFormat:@"<%@ - %p> : (x = %f, y = %f)", self.class, self, self.x, self.y];
}

- (double)distanceBetweenOriginalPoint {
return sqrt(pow(self.x, 2) + pow(self.y, 2));
}

@end

在上面的代码中,自定义了一个MyPointExport协议,该协议继承了JSExport协议,在这个协议中,声明了两个属性和一个方法,因此,我们可以在JavaScript代码中使用它们。

function.js文件中加入middlePointOfTwoPoints(p1, p2)JavaScript函数,在这个函数中,调用了协议中声明的属性及方法:

1
2
3
4
5
function middlePointOfTwoPoints(p1, p2) {
var x = (p1.x + p2.x) / 2.0;
var y = (p1.y + p2.y) / 2.0;
return Point.makePointWithXY(x, y);
}

在上面的JavaScript代码中,使用了Point类,它是与MyPoint类相关联的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
NSString *scriptPath = [[NSBundle mainBundle] pathForResource:@"functions" ofType:@"js"];
NSString *funcScript = [NSString stringWithContentsOfFile:scriptPath encoding:NSUTF8StringEncoding error:nil];

JSContext *ctx = [JSContext new];
[ctx evaluateScript:funcScript];

JSValue *functionValue = ctx[@"middlePointOfTwoPoints"];

ctx[@"Point"] = [MyPoint class];// 在调用相关JavaScript函数之前,配置相关的类

MyPoint *p1 = [MyPoint makePointWithX:100 y:100];
MyPoint *p2 = [MyPoint makePointWithX:300 y:300];

JSValue *pointValue = [functionValue callWithArguments:@[p1, p2]];
MyPoint *middlePoint = [pointValue toObject];

注意MyPoint类中的distanceBetweenOriginalPoint方法,因为该方法并没有在MyPointExport协议中进行声明,因此,在JavaScript代码中,是不能使用该方法的,否则就会产生异常。通过JSContextsetExceptionHandler:方法,可以捕获到异常:

1
2
3
4
JSContext *ctx = [JSContext new];
[ctx setExceptionHandler:^(JSContext *context, JSValue *exception) {
NSLog(@"exception: %@", exception);
}];

3. 内存管理

  • Objective-C使用ARC(你可能还在使用MRC)
  • JavaScriptCore使用垃圾回收,所有的引用都是强引用
  • JavaScriptCore API中的内存管理,多数是自动进行管理的

不过需要特别注意两种情形:

  1. 在Objective-C对象中存储JavaScript值
  2. 向Objective-C对象中添加JavaScript域

如何避免循环引用:
先使用JSManagedValue对JavaScript值进行弱引用,然后再使用JSVirtualMachineaddManagedReference:withOwner:方法,将JSManagedValue转入“垃圾回收”引用

4. 线程

JavaScriptCore API是线程安全的,锁粒度为JSVirtualMachine,因此对于并发/并行,需要使用多个JSVirtualMachine

单个进程可以包含多个JSVirtualMachine,单个JSVirtualMachine可以包含多个JSContext,单个JSContext可以包含多个JSValue

处于同一个JSVirtualMachine中的JSValue,可以在该JSVirtualMachine中的JSContext之间进行传递,但不能传递到其它JSVirtualMachineJSContext中。

5. JavaScript与WebView

要想通过JavaScriptCore框架让web view中的JavaScript与Objective-C进行交互,关键点就是获取web view中的JSContext

获取到JSContext后,就可以装载自定义对象,以及替换原有的回调函数。

对于WebView类,通过WebFrameLoadDelegate协议的webView:didCreateJavaScriptContext:forFrame:方法,可以获取到JSContext对象。

但对于UIWebView类,苹果官方并没有公开相关的方法。不过,可以使用下面的两种方式:

1
JSContext *ctx = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
  • 参照Mac OS X中WebFrameLoadDelegate协议的webView:didCreateJavaScriptContext:forFrame:方法,获取web view中的JSContextUIWebView-TS_JavaScriptContext

JavaScriptCore框架入门
https://daniate.github.io/2016/04/01/JavaScriptCore框架入门/
作者
Daniate
发布于
2016年4月1日
许可协议