JavaScriptCore使用

JavaScriptCore

JavaScriptCore是webkit的一个重要组成部分,主要是对JS进行解析和提供执行环境。代码是开源的,可以下下来看看(源码)。iOS7后苹果在iPhone平台推出,极大的方便了我们对js的操作。我们可以脱离webview直接运行我们的js。iOS7以前我们对JS的操作只有webview里面一个函数 stringByEvaluatingJavaScriptFromString,JS对OC的回调都是基于URL的拦截进行的操作。大家用得比较多的是WebViewJavascriptBridgeEasyJSWebView这两个开源库,很多混合都采用的这种方式。

JavaScriptCore和我们相关的类不是很多,使用起来也非常简单。

1
2
3
4
5
#import "JSContext.h"
#import "JSValue.h"
#import "JSManagedValue.h"
#import "JSVirtualMachine.h"
#import "JSExport.h"

JSContext

JSContext 是JS代码的执行环境

JSContext 为JS代码的执行提供了上下文环境,通过jSCore执行的JS代码都得通过JSContext来执行。

JSContext对应于一个 JS 中的全局对象

JSContext对应着一个全局对象,相当于浏览器中的window对象,JSContext中有一个GlobalObject属性,实际上JS代码都是在这个GlobalObject上执行的,但是为了容易理解,可以把JSContext等价于全局对象。

JSValue

JSValue 是对 JS 值的包装

JSValue 顾名思义,就是JS值嘛,但是JS中的值拿到OC中是不能直接用的,需要包装一下,这个JSValue就是对JS值的包装,一个JSValue对应着一个JS值,这个JS值可能是JS中的number,boolean等基本类型,也可能是对象,函数,甚至可以是undefined,或者null。转换如下:

1
2
3
4
5
6
7
8
9
10
11
12
Objective-C type  |   JavaScript type
--------------------+---------------------
nil | undefined
NSNull | null
NSString | string
NSNumber | number, boolean
NSDictionary | Object object
NSArray | Array object
NSDate | Date object
NSBlock (1) | Function object (1)
id (2) | Wrapper object (2)
Class (3) | Constructor object (3)

其实,就相当于JS 中的 var。

JSValue存在于JSContext中

JSValue是不能独立存在的,它必须存在于某一个JSContext中,就像浏览器中所有的元素都包含于Window对象中一样,一个JSContext中可以包含多个JSValue。

都是强引用

这点很关键,JSValue对其对应的JS值和其所属的JSContext对象都是强引用的关系。因为jSValue需要这两个东西来执行JS代码,所以JSValue会一直持有着它们。

通过下面这些方法来创建一个JSValue对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
+ (JSValue *)valueWithObject:(id)value inContext:(JSContext *)context;

+ (JSValue *)valueWithBool:(BOOL)value inContext:(JSContext *)context;

+ (JSValue *)valueWithDouble:(double)value inContext:(JSContext *)context;

+ (JSValue *)valueWithInt32:(int32_t)value inContext:(JSContext *)context;

+ (JSValue *)valueWithUInt32:(uint32_t)value inContext:(JSContext *)context;

+ (JSValue *)valueWithNewObjectInContext:(JSContext *)context;

+ (JSValue *)valueWithNewArrayInContext:(JSContext *)context;

+ (JSValue *)valueWithNewRegularExpressionFromPattern:(NSString *)pattern flags:(NSString *)flags inContext:(JSContext *)context;

+ (JSValue *)valueWithNewErrorFromMessage:(NSString *)message inContext:(JSContext *)context;

+ (JSValue *)valueWithNullInContext:(JSContext *)context;

+ (JSValue *)valueWithUndefinedInContext:(JSContext *)context;

你可以将OC中的类型,转换成JS中的对应的类型(参见前面那个类型对照表),并包装在JSValue中,包括基本类型,Null和undfined。

或者你也可以创建一个新的对象,数组,正则表达式,错误,这几个方法达到的效果就相当于在JS中写 var a = new Array();

也可以将一个OC对象,转成JS中的对象,但是这样转换后的对象中的属性和方法,在JS中是获取不到的,怎样才能让JS中获取的OC对象中的属性和方法,我们后面再说。

OC调用JS

首先是一段JS代码,一个简单的递归函数,计算阶乘的:

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

OC中调用这个JS中的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//初始化JSContext
self.context = [[JSContext alloc] init];
//这句的效果就相当于在一个全局对象中声明了一个叫fatorial的函数,但是没有调用它,只是声明,所以执行完这段JS代码后没有返回值
[self.context evaluateScript:[self loadJsFile:@"OCCallJS"]];

NSNumber *inputNumber = [NSNumber numberWithInteger:[self.textField.text integerValue]];

//通过声明的JS方法名获取到这个函数
JSValue *function = [self.context objectForKeyedSubscript:@"factorial"];
//执行这个函数
JSValue *result = [function callWithArguments:@[inputNumber]];
//转换成对应的OC数据类型打印
NSLog(@"%d",[result toInt32]);

// 方法二.
// JSValue * function = self.context[@"factorial"];
// JSValue *result = [function callWithArguments:@[inputNumber]];
// NSLog(@"%d",[result toInt32]);

首先,从bundle中加载这段JS代码。

然后,创建一个JSContext,并用他来执行这段JS代码,这句的效果就相当于在一个全局对象中声明了一个叫fatorial的函数,但是没有调用它,只是声明,所以执行完这段JS代码后没有返回值。

再从这个全局对象中获取这个函数,这里我们用到了一种类似字典的下标写法来获取对应的JS函数,就像在一个字典中取这个key对应的value一样简单,实际上,JS中的对象就是以 key : Value 的形式存储属性的,且JS中的object对象类型,对应到OC中就是字典类型,所以这种写法自然且合理。

这种类似字典的下标方式不仅可以取值,也可以存值。不仅可以作用于Context,也可以作用与JSValue,他会用中括号中填的key值去匹配JSValue包含的JS值中有没有对应的属性字段,找到了就返回,没找到就返回undefined。

然后,我们拿到了包装这个阶乘函数的的JSValue对象,在其上调用callWithArguments方法,即可调用该函数,这个方法接收一个数组为参数,这是因为JS中的函数的参数都不是固定的,我们构建了一个数组,并把NSNumber类型的值传了过去,然而JS肯定是不知道什么是NSNumber的,但是别担心,JSCore会帮我们自动转换JS中对应的类型, 这里会把NSNumber类型的值转成JS中number类型的值,然后再去调用这个函数。

最后,如果函数有返回值,就会将函数返回值返回,如果没有返回值则返回undefined,当然在经过JSCore之后,这些JS中的类型都被包装成了JSValue,最后我们拿到返回的JSValue对象,转成对应的类型并输出。

JS调用OC

JavaScript 与 Objective-C 交互主要通过2种方式:

1.Block : 第一种方式是使用block,block也可以称作闭包和匿名函数,使用block可以很方便的将OC中的单个方法暴露给JS调用。

2.JSExport 协议 : 第二种方式,是使用JSExport协议,可以将OC的中某个对象直接暴露给JS使用,而且在JS中使用就像调用JS的对象一样自然。

Block方式

首先看Html和JS代码:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, target-densitydpi=device-dpi">
<title>JS调用OC</title>
<style>
*
{
//-webkit-tap-highlight-color: rgba(0,0,0,0);
text-decoration: none;
}
html,body
{
-webkit-touch-callout: none; /* prevent callout to copy image, etc when tap to hold */
-webkit-text-size-adjust: none; /* prevent webkit from resizing text to fit */
-webkit-user-select: none; /* prevent copy paste, to allow, change 'none' to 'text' */
}
#div-a
{
background:#FBA;
color:#FFF;
}
</style>
<script type="text/javascript">

function showResult(resultNumber)
{
//alert(resultNumber);
document.getElementById("result").innerText = resultNumber;
}

</script>
</head>
<body style="background: #CDE; color: #FFF">
<div>
<!--title-->
<font size="3" color="black">输入一个整数:</font>
<!--输入框-->
<textarea id="input" style="font-size: 10pt; color: black"></textarea>
</div>

<div>
<font size="3" color="black">结果: <b id="result"> </b> </font>
</div>
<br/>

<div id="div-a">
<center>
<br/>
<input type="button" value="计算阶乘" onclick="native.calculateForJS(input.value);" />
<br/>

<br/>
<input type="button" value="测试log" onclick="log('测试');" />
<br/>

<br/>
<input type="button" value="oc原生Alert" onclick="alert('alert');" />
<br/>

<br/>
<input type="button" value="addSubView" onclick="addSubView('view');" />
<br/>

<br/>
<input type="button" value="多参数调用" onclick="mutiParams('参数1','参数2','参数3');" />
<br/>

<br/>
<a id="push" href="#" onclick="native.pushViewControllerTitle('SecondVC','secondPushedFromJS');">
push to second ViewController
</a>
<br/>
<br/>
</center>
</div>

</body>
</html>

OC代码

加载上方的Html代码

1
2
3
4
NSString *path = [[[NSBundle mainBundle] bundlePath]  stringByAppendingPathComponent:@"JS调用OC.html"];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL fileURLWithPath:path]];
self.testWebView.delegate = self;
[self.testWebView loadRequest:request];

实现WebView的加载完成代理

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
- (void)webViewDidFinishLoad:(UIWebView *)webView{
// 以 html title 设置 导航栏 title
self.title = [webView stringByEvaluatingJavaScriptFromString:@"document.title"];

// 禁用 页面元素选择
[webView stringByEvaluatingJavaScriptFromString:@"document.documentElement.style.webkitUserSelect='none';"];

// 禁用 长按弹出ActionSheet
[webView stringByEvaluatingJavaScriptFromString:@"document.documentElement.style.webkitTouchCallout='none';"];

//初始化content
self.context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

// 打印异常,由于JS的异常信息是不会在OC中被直接打印的,所以我们在这里添加打印异常信息
self.context.exceptionHandler = ^(JSContext *context, JSValue *exception) {
context.exception = exception;
NSLog(@"___%@",exception);
};

// 以 JSExport 协议关联 native 的方法,这是用JSExport协议调用必须实现的
self.context[@"native"] = self;

// 以 block 形式关联 JavaScript function
self.context[@"log"] = ^(NSString *str){
NSLog(@"++++%@",str);
};

// 以 block 形式关联 JavaScript function
__weak __typeof(self)weakSelf = self;
self.context[@"alert"] =
^(NSString *str)
{
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"msg from js" message:str preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:nil]];
[weakSelf presentViewController:alertController animated:YES completion:nil];
};

self.context[@"addSubView"] =
^(NSString *viewname)
{
//异步调用需要回调到主线程,不然控制台会打印警告log
dispatch_async(dispatch_get_main_queue(), ^{
UIView *testView = [[UIView alloc] initWithFrame:CGRectMake(0, 64, 100, 100)];
testView.backgroundColor = [UIColor redColor];
UISwitch *sw = [[UISwitch alloc]init];
[testView addSubview:sw];
[weakSelf.view addSubview:testView];
});
};
//多参数
self.context[@"mutiParams"] =
^(NSString *a,NSString *b,NSString *c)
{
NSLog(@"%@ %@ %@",a,b,c);
};

//直接调用JS的方法
[self.context evaluateScript:@"mutiParams"];
}

使用 Block 的坑

使用Block暴露方法很方便,但是有2个坑需要注意一下:

1.不要在Block中直接使用JSValue

2.不要在Block中直接使用JSContext

因为Block会强引用它里面用到的外部变量,如果直接在Block中使用JSValue的话,那么这个JSvalue就会被这个Block强引用,而每个JSValue都是强引用着它所属的那个JSContext的,这是前面说过的,而这个Block又是注入到这个Context中,所以这个Block会被context强引用,这样会造成循环引用,导致内存泄露。不能直接使用JSContext的原因同理。

那怎么办呢,针对第一点,建议把JSValue当做参数传到Block中,而不是直接在Block内部使用,这样Block就不会强引用JSValue了。

针对第二点,可以使用[JSContext currentContext] 方法来获取当前的Context。

JSExport协议

在.h声明JSExport协议

1
2
3
4
5
6
7
//声明JSExport协议
@protocol TestJSExport <JSExport>
JSExportAs(calculateForJS /** handleFactorialCalculateWithNumber 作为js方法的别名 */,
- (void)handleFactorialCalculateWithNumber:(NSNumber *)number
);
- (void)pushViewController:(NSString *)view title:(NSString *)title;
@end

此协议声明了两个方法

1
2
3
- (void)handleFactorialCalculateWithNumber:(NSNumber *)number

- (void)pushViewController:(NSString *)view title:(NSString *)title;

对应的会通过JSCore转换成JScalculateForJS的方法和pushViewControllerTitle方法

需要注意的是,OC中的函数声明格式与JS中的不太一样(应该说和大部分语言都不一样。。),OC函数中多个参数是用冒号:声明的,这显然不能直接暴露给JS调用,这不高保真。

所以需要对带参数的方法名做一些调整,当我们暴露一个带参数的OC方法给JS时,JSCore会用以下两个规则生成一个对应的JS函数:

1.移除所有的冒号

2.将跟在冒号后面的第一个小写字母大写

比如上面的那个类方法,转换之前方法名应该是 pushViewController: title:,在JS中生成的对应的方法名就会变成 pushViewControllerTitle。

苹果知道这种不一致可能会逼死某些强迫症。。所以加了一个宏JSExportAs来处理这种情况,它的作用是:给JSCore在JS中为OC方法生成的对应方法指定名字。

比如,还是上面这个方法handleFactorialCalculateWithNumber: number:,可以这样写:calculateForJS

注意:这个宏只对带参数的OC方法有效。

在.m里面实现声明的方法

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
36
37
38
39
40
41
42
43
注意这一句话
// 以 JSExport 协议关联 native 的方法
self.context[@"native"] = self;

#pragma mark - JSExport Methods
//实现JSExport协议声明的计算结果方法
- (void)handleFactorialCalculateWithNumber:(NSNumber *)number
{
NSLog(@"%@", number);

NSNumber *result = [self calculateFactorialOfNumber:number];

NSLog(@"%@", result);

[self.context[@"showResult"] callWithArguments:@[result]];
}
//实现JSExport协议声明的Push页面方法
- (void)pushViewController:(NSString *)view title:(NSString *)title
{
dispatch_async(dispatch_get_main_queue(), ^{
Class second = NSClassFromString(view);
UIViewController *secondVC = (UIViewController *)[[second alloc]init];
secondVC.title = title;
[self.navigationController pushViewController:secondVC animated:YES];
});
}
#pragma mark - Factorial Method
- (NSNumber *)calculateFactorialOfNumber:(NSNumber *)number
{
NSInteger i = [number integerValue];
if (i < 0)
{
return [NSNumber numberWithInteger:0];
}
if (i == 0)
{
return [NSNumber numberWithInteger:1];
}

NSInteger r = (i * [(NSNumber *)[self calculateFactorialOfNumber:[NSNumber numberWithInteger:(i - 1)]] integerValue]);

return [NSNumber numberWithInteger:r];
}

内存管理

我们都知道,Objective-C 用的是ARC (Automatic Reference Counting),不能自动解决循环引用问题(retain cycle),需要程序员手动处理,而JavaScript 用的是GC (准确的说是 Tracing Garbage Collection),所有的引用都是强引用,但是垃圾回收器会帮我解决循环引用问题,JavaScriptCore 也一样,一般来说,大多数时候不需要我们去手动管理内存。

但是下面2种情况需要注意一下:

1.不要在JS中给OC对象增加成员变量,这句话的意思就是说,当我们将一个OC对象暴露给JS后,就像前面说的使用JSExport协议,我们能想操纵JS对象一样操纵OC对象,但是这时候,不要在JS中给这个OC对象添加成员变量,因为这个动作产生的后果就是,只会在JS中为这个OC对象增加一个额外的成员变量,但是OC中并不会同步增加。所以说这样做并没有什么意义,还有可能造成一些奇怪的内存管理问题。

2.OC对象不要直接强引用JSValue对象,这句话再说直白点,就是不要直接将一个JSValue类型的对象当成属性或者成员变量保存在一个OC对象中,尤其是这个OC对象还暴露给JS的时候。这样会造成循环引用。

如何解决这个问题呢?你可能会想,不能强引用, 那就弱引用呗,但是这样做也是不行的,因为JSValue没用对象引用他,他就会被释放了。

那怎么办?分析一下,在这里,我们需要一种弱的引用关系,因为强引用会造成循环引用,但是又不能让这个JSValue因无人引用它而被释放。简而言之就是,弱引用但能保持JSValue不被释放。

于是,苹果退出了一种新的引用关系,叫conditional retain,有条件的强引用,通过这种引用就能实现我们前面分析所需要的效果,而JSManagedValue就是苹果用来实现conditional retain的类。

JSManagedValue

这是JSManagedValue的一般使用步骤:

1
2
JSManagedValue *managedValue = [JSManagedValue managedValueWithValue:jsValue];
[self.context.virtualMachine addManagedReference:managedValue withOwner:self];

1.首先,用JSValue创建一个JSManagedValue对象,JSManagedValue里面其实就是包着一个JSValue对象,可以通过它里面一个只读的value属性取到,这一步其实是添加一个对JSValue的弱引用。

2.如果只有第一步,这个JSValue会在其对应的JS值被垃圾回收器回收之后被释放,这样效果就和弱引用一样,所以还需要加一步,在虚拟机上为这个JSManagedValue对象添加Owner(这个虚拟机就是给JS执行提供资源的,待会再讲),这样做之后,就给JSValue增加一个强关系,只要以下两点有一点成立,这个JSManagedValue里面包含的JSValue就不会被释放:

JSValue对应的JS值没有被垃圾回收器回收

Owner对象没有被释放

看个Demo

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
36
37
38
39
40
41
42
43
44
45
46
47
48
//定义一个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;
obj.jsValue=callback;
}"];
//调用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 帮助我们保存JSValue,那里面保存的JS对象必须在JS中存在,同时 JSManagedValue 的owner在OC中也存在。

创建JSManagedValue的两种方法

1
2
+ (JSManagedValue *)managedValueWithValue:(JSValue *)value;
+ (JSManagedValue *)managedValueWithValue:(JSValue *)value andOwner:(id)owner

建立弱引用关系方法

1
- (void)addManagedReference:(id)object withOwner:(id)owner;

移除弱引用方法

1
- (void)removeManagedReference:(id)object withOwner:(id)owner;

修改代码如下

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
//定义一个JSExport protocol
@protocol JSExportTest <JSExport>
//用来保存JS的对象
@property (nonatomic, strong) JSValue *jsValue;

@end

//建一个对象去实现这个协议:

@interface JSProtocolObj : NSObject<JSExportTest>
//添加一个JSManagedValue用来保存JSValue
@property (nonatomic, strong) JSManagedValue *managedValue;

@end

@implementation JSProtocolObj

@synthesize jsValue = _jsValue;
//重写setter方法
- (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]];

}

最后一点

一个 JSVirtualMachine可以运行多个context,由于都是在同一个堆内存和同一个垃圾回收下,所以相互之间传值是没问题的。但是如果在不同的 JSVirtualMachine传值,垃圾回收就不知道他们之间的关系了,可能会引起异常。

本文章Demo地址

参考:http://www.cocoachina.com/ios/20170720/19958.html