When iOS loves JS

1 JSBinding概述

说明:JSBindingJSnative之间的一个桥梁,通过这个桥梁,JS可以调用NativeNative可以调用JS

Alt text

注意:JSBinding不是什么

  • not Hybrid
  • not a new technology

Alt text

历史:cocos2D-X使用zynga提出的一个方案(https://github.com/zynga/jsbindings),`JS Engine使用的是SpiderMonkey`。

1.1 JS和Native

说明:两者结合初衷如下

原生静态语言 动态脚本语言
高性能 简单易用
更底层,更强大 免编译
平台特性 热部署

1.2 iOS 7 and JavaScriptCore

说明:iOS 7之后,苹果公布了JavaScriptCoreAPI,在这之前一直是iOS系统私有的。
扩展:MacJavaScriptCore一直是开放的。
要点:包括以下部分

  • JavaScriptCore.h头文件
  • JSContext类:JS的运行环境
  • JSValue类:
  • JSExport

1.2.1 Eval JavaScript Code

说明:利用引擎执行JS代码

1
2
3
4
5
6
7
8
9
10
11
#import <JavaScriptCore/JavaScriptCore.h>
int main(int argc, char * argv[]) {
// 创建 JS 运行的上下文
JSContext *context = [[JSContext alloc] init];
// 使用 JS 上下文执行 JS 语句 "1 + 2",并返回计算结果
JSValue *result = [context evaluateScript: @"1 + 2"];
// 打印计算结果
NSLog(@"1 + 2 = %f", [result toDouble]);

return 0;
}

1.2.2 Call JavaScript Function

说明:假设一个叫做sumJS方法已经被加载

1
2
3
4
5
6
7
// a "sum" function was loaded in context
// 使用上下文获取函数 sum
JAValue *sum = context[@"sum"];
// 调用sum JS 方法,并传递两个参数
JSValue *result = [sum callWithArguments:@[@1, @2]];
// 打印计算结果
NSLog(@"sum(1, 2) = %f", [result toDouble]);

1.2.3 Create JavaScript Value

说明:可以先创建JS的值,再赋值给相应变量。

1
2
3
4
5
6
// 在指定上下文中创建值 231
JSValue *intVar = [JSValue valueWithInt32: 231 inContext: context];
// 将值赋给变量 bar
context[@"bar"] = intVar;
//
[context evaluateScript: @"bar++];

another way

1
[context evaluateScript: @"var bar = 231;bar++;"];

1.2.4 Type Conversion

说明:JSO-C之间对应类型的转换

Objective-C type JavaScript type
nil undefined
NSNull null
NSString string
NSNumber number, boolean
NSDictionary Object object
NSArray Array object
NSDate Date object
NSBlock Function object
id Wrapper object
Class Constructor object

1.3 完整实例

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
#import <JavaScriptCore/JavaScriptCore.h>
int main(int argc, char * argv[]) {
// 创建 JS 运行的上下文
JSContext *context = [[JSContext alloc] init];
// 为上下文指定JS异常处理代码块
context.exceptionHandler = ^(JSContext *ctx, JSValue *exception) {
NSLog(@"%@", exception);
};
NSString *script;
JSValue *result;

// 表达式
script = @"1 + 2 + 3";
result = [context evaluateScript:script];
NSLog(@" %@ = %f", script, [result toDouble]);

// 语句(创建一个全局变量)
script = @"var globalVal = 2 * 3;";
result = [context evaluateScript:script];
NSLog(@" globalVar = %@", context[@"globalVar"]);

// 函数
script = @"function sum(a, b){return a + b;}";
[context evaluateScript:script];
JSValue *sum = context[@"sum"];
result = [sum callWithArguments:@[@1, @2]];
NSLog(@"Result of %@ is %f", @"sum(1,2)", [result toDouble]);

// 创建值
JSValue *foo = [JSValue valueWithDouble:123.45 inContext:context];
context[@"foo"] = foo;
[context evaluateScript:@"foo++"];
NSLog(@"foo = %f", [context[@"foo"] toDouble]);
return 0;
}

2 初始iOS7 JavaScriptCore API

2.1 Call Native Code via Block

说明:JS调用Native

1
2
3
4
5
6
7
8
9
//------------ JS 调用 Native
// 为 JS 的上下文注入可以调用的本地 BLOCK
context[@"sum"] = ^(int a, int b) {
return a + b;
};
// 在 JS 中调用 Native block
result = [context evaluateScript:@"sum(1, 2)"];
// 观察运行结果
NSLog(@"sum(1, 2) = %f", [result toDouble]);

2.2 Call Native Code via JSExport

说明:通过jsExport调用Native

  • Create a js export Class in Objective-C(创建jsExport类)
  • the instance of this class can be accessed in js context(该类的实例可以在js的上下文中访问到)
  • No Constructor in JS(无法在JS中访问到JSExport对象的构造函数)

###实例

说明:需要做以下工作

  1. 定义一个JSExport的子协议:协议指定的成员才能够暴露给JS上下文
  2. 创建一个O-C类,并采纳协议

Point3D.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#import <Foundation/Foundation.h>
// 协议:继承 JSExport
@protocol Point3DExport <JSExport>

@property float x;
@property float y;
@property float z;

- (double) length;

@end

// 采纳协议
@interface Point3D : NSObject <Point3DExport>
{
JSContext *context;
}

- (id) initWithContext: (JSContext *) context;

@end

Point3D.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#import <JavaScriptCore/JavaScriptCore.h>
#import "Point3D.h"
@implementation Point3D

@synthesize x;
@synthesize y;
@synthesize z;

- (id) initWithContext: (JSContext *) ctx
{
if (self = [super init]) {
context = ctx;
context[@"Point3D"] = [Point3D class];
}
return self;
}

- (double)length {
return sqrt(self.x * self.x + self.y * self.y + self.z * self.z);
}

@end

main.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
NSString *script;
JSValue *result;
// 创建实例
Point3D *point3D = [[Point3D alloc] initWithContext:context];
point3D.x = 1;
point3D.y = 2;
point3D.z = 3;
// 将实例注入到 JS 上下文
context[@"point3D"] = point3D;
// 待执行的 JS 脚本
script = @"point3D.x = 2; point3D.y = 2; point3D.length();";
// 在 JS 上下文中调用该实例
result = [context evaluateScript:script];
// 观察 JS 运行结果
NSLog(@"Result of %@ is %f", script, [result toDouble]);

2.3 载入js文件来执行

说明:加入js文件比起在Native代码中创建js脚本子串要通用、易用的多。

1
2
3
4
5
6
7
8
9
// 创建文件路径
NSString *filePath = [NSString stringWithFormat:
@"%@/%@",
[[NSBundle mainBundle] resourcePath],
fileName];
// 载入 js 文件
NSString *script = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
// 执行 js
[context evaluateScript:script];

3 JavaScriptCore API 进阶

3.1 Memory Leak

说明:js中对象的循环引用o-c中对象的循环引用都不会导致内存泄漏。但js对象和Native对象之间的循环引用会导致内存泄漏。

  • js对象之间循环引用:不会内存泄漏
    Alt text
  • o-c对象和js对象之间循环引用:会内存泄漏
    Alt text

解决:o-c对象引用js对象时将后者包装为JSManageredValue
Alt text
JS Code

1
2
3
var girl = new Girl();
girl.boyFriend = boy;
boy.girlFriend = girl;

Objective-C Code

1
2
3
4
5
6
7
8
@implementation Boy
- (void)setGirlFriend: (JSValue *)girl
{
// 使用 JSManageredValue 进行包装
self.girlFriend = [JSManageredValue manageredValueWithValue: girl];
}
...
@end

3.2 Threding and parallelism

说明:要点

  • API is thread safe(API是线程安全的)
  • Locking granularity is JSVirtualMachine(锁的最小粒度为是JS虚拟机)
  • Use separate JSVirtualMachines for parallelism(并发时要使用不同的JS虚拟机)
    ###实例
    说明:创建了2个虚拟机实例
  • jsvmA:运行了两个js上下文(ctxA1ctxA2)
  • jsvmB:运行了一个js上下文(ctxB)

ctxA1ctxA2能够在并发情形下正常通信,但他们ctxB在并发环境下不能通信。因为前两者在同一个虚拟机中运行,也就意味着运行在同一个线程中。

1
2
3
4
5
6
7
8
9
10
11
// 创建 js 虚拟机实例 jsvmA
JSVirtualMachine *jsvmA = [[JSVirtualMachine alloc] init];

// 在 jsvmA 虚拟机中创建两个js上下文
JSContext *ctxA1 = [[JSContext alloc] initWithVirtualMachine: jsvmA];
JSContext *ctxA2 = [[JSContext alloc] initWithVirtualMachine: jsvmA];

// 创建另一个 js 虚拟机实例 javmB
JSVirtualMachine *jsvmB = [[JSVirtualMachine alloc] init];
// // 在 jsvmB 虚拟机中创建一个js上下文
JSContext *ctxB = [[JSContext alloc] initWithVirtualMachine: jsvmB];

3.3 JSBinding in the Real World

说明:JSBinding的一些实际应用。
Alt text

  • Cocos2D
  • Ejecta
  • CocoonJS
    Alt text
  • node.app

    3.3.1 JS Engines

说明:2种JS引擎比较,JavaScriptCoreSpiderMonkey

  • JavaScriptCore
  • SpiderMonkey(Cocos2D-iPhone-2.1-bate3)
  • SpiderMonkey(Cocos2D-x-2.1-bate3)
  • SpiderMonkey(iMonkey)

注意:v8引擎不受支持,因为其依赖于JIT,被苹果禁用。
Alt text

3.3.2 JSBind VS LuaBinding

说明:JSBindingLuaBinding比较

  • Native
  • Web
  • Hybrid
  • ScriptBinding