总览
在iOS的开发过程中,除了使用oc和swift语言开发原生应用, iOS还支持使用js与原生进行交互,比如热修复应用JSPatch就是使用js代码进行原生代码的替换。代码之所以可以动态替换,是因为oc的runtime机制,js代码之所以可以跟被iOS识别,则是因为JavaScriptCore。所以,总的来说,JavaScriptCore的主要作用是建立js与iOS原生之间的交互。 既然是交互,那就肯定有js调用原生和原生调用js方法,以下以oc为例,学习这两种调用方式。 首先附上DEMO地址和JavaScriptCore的源码地址。
网上已经有很多文章提到了JavaScriptCore暴露的几个类,我也要把这几个类再贴一下,因为用到的确实就这几个类。。。
#import "JSContext.h"
#import "JSValue.h"
#import "JSManagedValue.h"
#import "JSVirtualMachine.h"
#import "JSExport.h"
Objective-C -> JavaScript
想要调用js方法,那首先得有个方法。先建立一个js文件,取名factorial.js。里面填入下面内容:
var factorial = function(n) {
if (n < 0)
return ;
if (n === 0)
return 1;
return n * factorial(n - 1)
};
然后写个方法来调它:
JSContext *context = [[JSContext alloc] init];
context = [[JSContext alloc] init];
NSString *path = [[NSBundle mainBundle] pathForResource:@"factorial" ofType:@"js"];
NSString *factorialScript = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
NSLog(@"factorialScript : %@",factorialScript);
[context evaluateScript:factorialScript];
JSValue *function = context[@"factorial"];
NSLog(@"function: %@",function);
JSValue *result = [function callWithArguments:@[@5]];
NSLog(@"factorial(5) = %d", [result toInt32]);
上面的代码中出现了上面几个.h文件中的一些概念:JSContext和JSValue。JSContext是js的运行环境。主要作用是执行js代码和注册oc方法接口。JSValue是JSContext的返回结果,它是oc和js之间数据通信的桥梁。在实现上JSValue指向一个js的值。每一个JSValue都归属于某个JSContext。JSContext通过调用evaluateScript:方法来执行一段js脚本。之后,即可以通过[]的方式来获得js中的方法名。再利用callWithArguments:方法就可以执行js方法并返回一个JSValue类型的值。
JavaScript -> Objective-C
js调用oc代码有两种方式,一种是通过block,一种是通过oc类实现JSExport协议。
先来看block:
context[@"useBlock"] = ^(NSString *word) {
NSLog(@"useBlock : %@",word);
};
相应的,要在js中写调用此方法的代码:
var useBlockInJS = function(word) {
return useBlock(word);
};
方法写完后,在oc中执行这个js方法:
JSValue *useBlockInJS = context[@"useBlockInJS"];
[useBlockInJS callWithArguments:@[@"useBlockInJS"]];
整个过程很简单,首先利用context注册了一个block方法,然后在js中就可以直接调用这个方法。但是在使用block的时候需要注意一些问题,比如不要直接在block使用context和JSValue的值,因为这样很容易循环引用。例如以下的情况:
//由于block强引用context,context 又强引用block,所以循环引用
context[@"useBlock"] = ^(NSString *word) {
NSLog(@"useBlock : %@ in context : %@",word,context);
};
//由于context强引用block,block强引用useBlockInJS,useBlockInJS也强引用context,所以形成引用三角,循环引用
context[@"useBlock"] = ^(NSString *word) {
NSLog(@"useBlock : %@ and the function is : %@",word, [useBlockInJS toObject]);
};
下面介绍oc类实现JSExport协议的方式:
这是一个实现了JSExport的protocol,里面我们定义了一些property和method。
@protocol MyPointExports
@property double x;
@property double y;
- (NSString *)description;
+ (MyPoint *)makePointWithX:(double)x y:(double)y;
@end
js里面的实现代码为:
var euclideanDistance = function(p1,p2) {
var xDelta = p2.x - p1.x;
var yDelta = p2.y - p1.y;
return Math.sqrt(xDelta * xDelta + yDelta * yDelta);
};
var midpoint = function(p1, p2) {
log("p1.x = " + p1.x + " p1.y =" + p1.y);
log("p2.x = " + p2.x + " p2.y =" + p2.y);
var xDelta = (p2.x - p1.x) / 2;
var yDelta = (p2.y - p1.y) / 2;
return MyPoint.makePointWithXY(p1.x + xDelta, p1.y + yDelta);
};
这是类的使用代码,MyPoint的实现代码有兴趣请看Demo,里面只是几句简单的method实现代码。
MyPoint *point1 = [[MyPoint alloc] initWithX:10.0 y:50.0];
MyPoint *point2 = [[MyPoint alloc] initWithX:13.0 y:53.0];
JSValue *euclideanDistanceFunction = context[@"euclideanDistance"];
JSValue *euclideanDistanceResult = [euclideanDistanceFunction callWithArguments:@[point1,point2]];
NSLog(@"euclideanDistanceResult : %f",[euclideanDistanceResult toDouble]);
//如果想在js中使用整个类,只需要把类赋给context,如下:
context[@"MyPoint"] = [MyPoint class];
JSValue *midpointFunction = context[@"midpoint"];
JSValue *jsResult = [midpointFunction callWithArguments:@[point1, point2]];
MyPoint *midpoint = [jsResult toObject];
NSLog(@"midpoint's x: %f, midpoint's y: %f",midpoint.x,midpoint.y);
通过[MyPoint class]把类赋给context,这样在protocol中的方法都可以在js中使用,整个过程也是很简单也很明了的。
截止目前,前面给出的5个.h文件只用了3个,还剩两个一直没提:JSVirtualMachine和JSManagedValue。JSVirtualMachine提供的是JS运行的虚拟机。每个JSVirtualMachine都有独立的堆空间和垃圾回收机制。JSManagedValue的作用是辅助管理js和oc对象。之所以存在JSManagedValue,是因为js内存管理采用的是垃圾回收机制,而oc采用的是引用计数。如果两方互相引用,很容易造成循环引用。具体可以看例子:
@interface MyButton : UIButton
@property (nonatomic,strong) JSContext *context;
@property (nonatomic, strong) JSManagedValue *onClickHandler;
//@property (nonatomic, strong) JSValue *onClickHandler;
- (void)configureOnClickHandler:(JSValue *)onClickHandler;
@end
@implementation MyButton
- (void)configureOnClickHandler:(JSValue *)onClickHandler {
_onClickHandler = onClickHandler;
}
//使用代码
JSValue *clickHandlerFunction = context[@"ClickHandler"];
MyButton *button = [[MyButton alloc] init];
[button configureOnClickHandler:[clickHandlerFunction callWithArguments:@[button,^{
NSLog(@"clicked");
}]]];
js端代码为:
function ClickHandler(button, callback) {
this.button = button;
this.button.onClickHandler = this;
this.handleEvent = callback;
callback();
}
在这个例子中,MyButton持有一个JSValue类型的值onClickHandler,而ClickHandler也持有MyButton的引用,如下图所示:
双方都是强引用,所以造成循环引用。想要解除循环引用,就需要用到NSManagedValue,如下所示:
@interface MyButton : UIButton
@property (nonatomic,strong) JSContext *context;
@property (nonatomic, strong) JSManagedValue *onClickHandler;
//@property (nonatomic, strong) JSValue *onClickHandler;
- (void)configureOnClickHandler:(JSValue *)onClickHandler;
@end
@implementation MyButton
- (void)configureOnClickHandler:(JSValue *)onClickHandler {
//_onClickHandler = onClickHandler;
_onClickHandler = [JSManagedValue managedValueWithValue:onClickHandler];
[_context.virtualMachine addManagedReference:_onClickHandler withOwner:self];
NSLog(@"context virtual machine in MyButton: %@",_context.virtualMachine);
}
@end
将上面的JSValue改为JSManagedValue并将其添加到virtualMachine中, 此时再看下引用情况:
至于这两行代码的作用,我们可以通过这两个方法的实现来大致看一下:
+ (JSManagedValue *)managedValueWithValue:(JSValue *)value
{
return [[[self alloc] initWithValue:value] autorelease];
}
- (instancetype)initWithValue:(JSValue *)value
{
self = [super init];
if (!self)
return nil;
if (!value)
return self;
JSC::ExecState* exec = toJS([value.context JSGlobalContextRef]);
JSC::JSGlobalObject* globalObject = exec->lexicalGlobalObject();
JSC::Weak weak(globalObject, managedValueHandleOwner(), self);
m_globalObject.swap(weak);
JSC::JSValue jsValue = toJS(exec, [value JSValueRef]);
if (jsValue.isObject())
m_weakValue.setObject(JSC::jsCast(jsValue.asCell()), self);
else if (jsValue.isString())
m_weakValue.setString(JSC::jsCast(jsValue.asCell()), self);
else
m_weakValue.setPrimitive(jsValue);
return self;
}
- (void)addManagedReference:(id)object withOwner:(id)owner
{
object = getInternalObjcObject(object);
owner = getInternalObjcObject(owner);
if (!object || !owner)
return;
JSC::APIEntryShim shim(toJS(m_group));
NSMapTable *ownedObjects = [m_externalObjectGraph objectForKey:owner];
if (!ownedObjects) {
NSPointerFunctionsOptions weakIDOptions = NSPointerFunctionsWeakMemory | NSPointerFunctionsObjectPersonality;
NSPointerFunctionsOptions integerOptions = NSPointerFunctionsOpaqueMemory | NSPointerFunctionsIntegerPersonality;
ownedObjects = [[NSMapTable alloc] initWithKeyOptions:weakIDOptions valueOptions:integerOptions capacity:1];
[m_externalObjectGraph setObject:ownedObjects forKey:owner];
[ownedObjects release];
}
NSMapInsert(ownedObjects, object, reinterpret_cast(reinterpret_cast(NSMapGet(ownedObjects, object)) + 1));
}
通过以上方法的实现,我们大致可以看出,JSManagedValue是将JSValue的强引用转换为弱引用,应该类似于oc里面的Strong和weak的转化,后面添加到virtualMachine中,因为是为了virtualMachine的内存管理和与oc runtime引用计数功能相关。
关于线程相关
一个JSVirtualMachine可以拥有多个JSContext,同一个JSVirtualMachine内的多个JSContext可以共享值,
但是不同JSVirtualMachine之间不可以。原因在于每一个JSVirtualMachine都拥有自己独立的heap和garbage collection 。正如我们上面看到的源码:
object = getInternalObjcObject(object);
owner = getInternalObjcObject(owner);
if (!object || !owner)
return;
addManagedReference:withOwner:方法在处理JSValue之前,要先检查是是否是一个context中的, 如果不是,则不能处理。
所有JavaScriptCore的API都是线程安全的,所以不同的线程,同一时间JSVirtualMachine只会在一个线程运行。如果你想要获取同步或者并发操作,需要使用多个JSVirtualMachine。
参考
JavaScriptCore 使用
iOS JavaScriptCore使用
IOS7开发~JavaScriptCore (一)
IOS7开发~JavaScriptCore (二)
WWDC 2013 - Session 615 (Integrating JavaScript into Native Apps)