JavaScriptCore学习总结

总览

在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的引用,如下图所示:

JavaScriptCore学习总结_第1张图片
retainCycles.png

双方都是强引用,所以造成循环引用。想要解除循环引用,就需要用到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中, 此时再看下引用情况:

JavaScriptCore学习总结_第2张图片
garbagecollectedreference.png

至于这两行代码的作用,我们可以通过这两个方法的实现来大致看一下:

+ (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)

你可能感兴趣的:(JavaScriptCore学习总结)