JavaScriptCore是Safari的JavaScript引擎,在iOS7之后苹果开放了JavaScriptCore框架,开发者可以通过其提供的OC接口来使用JavaScriptCore。说白了就是它提供了执行JavaScript代码的能力,相当于一个JavaScript的虚拟机。JavaScriptCore是开源的,感兴趣可以研究一下:https://trac.webkit.org/browser/trunk/Source/JavaScriptCore
JavaScriptCore.h中包含了框架中几个比较重要的类:
#import "JSContext.h" //js上下文,执行js代码
#import "JSValue.h" //封装js数据类型
#import "JSManagedValue.h" //管理JSValue内存
#import "JSVirtualMachine.h" //JavaScript虚拟机,js底层执行环境
#import "JSExport.h" //导出OC对象
下面结合几个简单的例子,理解这些对象所扮演的角色:
1.JSContext和JSValue
//demo1.js
1+2*3
//objective-c
- (void)test1
{
JSVirtualMachine *vm = [[JSVirtualMachine alloc] init];
JSContext *ctx = [[JSContext alloc] initWithVirtualMachine:vm];
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"demo1" ofType:@"js"];
NSString *script = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
JSValue *value = [ctx evaluateScript:script];
NSLog(@"%d",[value toInt32]); //output:7
}
-
一个JSVirtualMachine是一个完整独立的JavaScript执行环境,实现并发执行和内存管理(GC)。而JSContext处理具体的JavaScript代码,每个JSContext都属于一个JSVirtualMachine,相同JSVirtualMachine内的JSContext之间可以互相传值,不同虚拟机之间则不能传值,因为它们有自己独立的堆空间和垃圾回收器。
JSValue代表一个JavaScript值,提供一些基本数据类型在js和native之间的转换。每个JSValue会强引用它所在的JSContext,所以只要有一个JSValue被持有,它的JSContext就会一直存在。而JSManagedValue是可以自动管理内存的Value对象,这点会在后面“内存管理”部分详细讨论。
2.访问js对象
//demo2.js
var a = 1+2+3+4+5;
var b = {
'red':255,
'blue':0,
'green':255
};
var c = [b];
//objective-c
- (void)test2
{
JSVirtualMachine *vm = [[JSVirtualMachine alloc] init];
JSContext *ctx = [[JSContext alloc] initWithVirtualMachine:vm];
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"demo2" ofType:@"js"];
NSString *script = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
[ctx evaluateScript:script];
// 访问js对象的三种方式
NSLog(@"[] %@",ctx[@"a"]);
NSLog(@"objectForKeyedSubscript %@",[ctx objectForKeyedSubscript:@"a"]);
NSLog(@"globalObject %@",[ctx.globalObject objectForKeyedSubscript:@"a"]);
// 同样也可以赋值
ctx[@"a"] = [JSValue valueWithInt32:90 inContext:ctx];
[ctx setObject:[JSValue valueWithInt32:90 inContext:ctx] forKeyedSubscript:@"a"];
[ctx.globalObject setObject:[JSValue valueWithInt32:90 inContext:ctx] forKeyedSubscript:@"a"];
NSLog(@"[] %@",ctx[@"a"]);
NSLog(@"objectForKeyedSubscript %@",[ctx objectForKeyedSubscript:@"a"]);
NSLog(@"globalObject %@",[ctx.globalObject objectForKeyedSubscript:@"a"]);
// object对应NSDictionary
JSValue *b = ctx[@"b"];
NSLog(@"%@",[b toDictionary]);
// array对应NSArray
JSValue *c = ctx[@"c"];
NSLog(@"%@",[c toArray]);
}
- oc访问js对象和对它的赋值都是通过key-value的方式,具体有三种写法:
1.[]方式,直接context[key]获取;
2.通过context的objectForKeyedSubscript方法
3.context.globalObject可以获取js全局对象 - jsObject对应NSDictionary,jsArray对应NSArray
3.block与js function
//demo3.js
var sum = 0;
//由native注入add和myLog方法
for (let i=0;i<=100;i++){
sum = add(sum,i);
}
myLog("sum is "+sum); //output: sum is 5050
//objective-c
- (void)test3
{
JSVirtualMachine *vm = [[JSVirtualMachine alloc] init];
JSContext *ctx = [[JSContext alloc] initWithVirtualMachine:vm];
ctx[@"myLog"] = ^(NSString *s){
NSLog(@"myLog:%@",s);
};
ctx[@"add"] = ^(NSInteger a, NSInteger b){
return a+b;
};
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"demo3" ofType:@"js"];
NSString *script = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
[ctx evaluateScript:script];
}
- native的block可以转换成为js中的function对象
- js function无法直接转成native的block,js function也是一个对象,而对于block参数个数、类型还有返回类型都是固定的。可以通过下面的方法来执行:
//JSValue.h
//JSValue本身是一个function,通过callWithArguments调用
- (JSValue *)callWithArguments:(NSArray *)arguments;
//以构造方法执行
- (JSValue *)constructWithArguments:(NSArray *)arguments;
//执行该JSValue对象的某个方法
- (JSValue *)invokeMethod:(NSString *)method withArguments:(NSArray *)arguments;
4.JSExport
JSExport提供一种声明式的方法将OC的类和其属性方法导出到JavaScript:
首先要定义一个协议继承自JSExport,在其中声明需要导出的属性和方法,实例类实现该协议并提供相关实现。
//objective-c
//MyView.h
#include <JavaScriptCore/JavaScriptCore.h>
@protocol MyViewExports <JSExport>
- (instancetype)initWithFrame:(CGRect)frame;
- (void)show;
@end
@interface MyView : UIView <MyViewExports>
@property(class,nonatomic,weak) UIViewController *vc;
@end
//objective-c
//MyView.m
#import "MyView.h"
@implementation MyView
static id _vc = nil;
- (instancetype)initWithFrame:(CGRect)frame;
{
if (self = [super initWithFrame:frame]) {
self.backgroundColor = UIColor.redColor;
}
return self;
}
- (void)show
{
[self.class.vc.view addSubview:self];
}
+(void)setVc:(UIViewController *)vc
{
_vc = vc;
}
+(UIViewController *)vc
{
return (UIViewController *)_vc;
}
@end
上面代码将一个native自定义的view导出到js,提供了初始化方法和-show方法,我们用一段js代码创建这么一个view放在屏幕上:
//demo4.js
var view = new MyView({x:0,y:0,width:200,height:300});
view.show();
这里有个细节,写过JSPatch应该都知道,像CGRect这种结构体,在js中应该全部展开来写,比如{x:0,y:0,width:200,height:300},而不能写成{origin:{x:0, y:0}, size:{width:200, height:300}}
执行的代码如下:
//objective-c
- (void)test4
{
MyView.vc = self;
JSContext *ctx = [[JSContext alloc] init];
ctx[@"MyView"] = [MyView class];
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"demo4" ofType:@"js"];
NSString *script = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
[ctx evaluateScript:script];
}
5.内存管理
oc的内存管理是引用计数,而JavaScript则是垃圾回收机制。之前有提到每个JSValue会强引用它的JSContext:
//JSValue.h
@property (readonly, strong) JSContext *context;
如果我们导出一个native对象到js,则它会被全局对象globalObject持有,如果在这个对象中强引用了JSContext或者JSValue(value持有context),就会造成循环引用。
尤其在Block中,直接使用外部的JSContext或JSValue就会造成循环引用,可以通过JSContext的类方法+ (JSContext *)currentContext来获得,或者当做参数传入。必要时使用JSManagedValue。JSManagedValue提供了自动管理内存的特性,相当于JSManagedValue引用了真正的JSValue,并可以通过+ (JSManagedValue *)managedValueWithValue:(JSValue *)value andOwner:(id)owner; 添加引用关系,当没有native引用关系,且在js环境中也被回收时,会释放JSValue并置为nil(官方JSManagedValue注释)。
简单说就是我们常用的weak引用,不能直接用weak是因为JSValue对应的js对象是由垃圾回收器(GC)管理的,如果使用weak可能在我们需要它的时候已经被回收。所以通过JSManagedValue实现了对JSValue的引用并在合适时机释放的机制。