JavaScriptCore的学习

主要目的是为之后学习JSPatch做一些基础的学习

2016-05-12 | 阅读

JavaScriptCore

在iOS7中 Apple引入了JavaScriptCore框架,该框架让Objective-CJavaScript代码直接交互.该框架只是基于webkit中以c/c++实现的JavaScriptCore的分装.

JavaScriptCore简单来说,就是一个OC上JavaScript运行环境,框架中有5个主要的类:

  • JSVirtualMachine : 为JavaScript的运行提供了底层资源.同时,虚拟机是线程安全的,因为其实单线程执行的
  • JSContext : 为JavaScript提供运行环境.所有的JS脚步在这个上下文中执行,这里可以用来管理对象,添加方法.
  • JSValue : 是JavaScript和Objective-C之间交互的桥梁,负责两端对象的互相转换.
  • JSManagedValue : 将JSValue转换为JSManagedValue,则运行时两侧都可以正常访问对象,避免一端释放一端仍然持有的状态.
  • JSExport : 导出OC的函数与属性供JavaScript使用,继承了这个协议的协议中定义的方法,就可以直接在JSContext中被使用.

JSVirtualMachine 和 JSContext

JSVirtualMachine为JS运行提供底层资源,JSContext为其提供运行环境.对于JSContext,可以以JVM进行初始化:

- (instancetype)initWithVirtualMachine:(JSVirtualMachine *)virtualMachine;

也可以直接初始化,初始化时为其生成一个新的JVM,也就是JSVirtualMachineJSContext绑定在一起,但是使用者一般直接调用JSContext来管理应用环境.可以在JSContext上直接执行脚本:

- (JSValue *)evaluateScript:(NSString *)script;
//在iOS8中,方法修改为:
- (JSValue *)evaluateScript:(NSString *)script withSourceURL:(NSURL *)sourceURL;
// 添加了一个sourceURL参数 , 用于debug和异常抛出时标记JS路径

JSContext的异常处理,通过设置属性exceptionHandler :

@property (copy) void(^exceptionHandler)(JSContext *context, JSValue *exception);

JSContext中类似window的全局对象:

@property (readonly, strong) JSValue *globalObject;

JSContext下有以下定义:

@interface JSContext (SubscriptSupport)

- (JSValue *)objectForKeyedSubscript:(id)key;

- (void)setObject:(id)object forKeyedSubscript:(NSObject <NSCopying> *)key;
@end

根据key来查询和设置JSContext中的JSValue,直接可以使用NSDictionary[]语法:

Person *person = [[Person alloc] init];
context[@"p"] = person;

也就是,对于类想要如此定义方法来使用[]语法,函数名必须定义成objectForKeyedSubscript:setObject:forKeyedSubscript:.

JSValue

JSValue是JS和OC之间交互的桥梁,它提供了很多方法来进行JS的数据类型与OC对象之间的互相转换:

Objective-C JavaScript JSValue Convert JSValue Constructor
nil undefined valueWithUndefinedInContext
NSNull null valueWithNullInContext:
NSString string toString
NSNumber number, boolean toNumber
toBool
toDouble
toInt32
toUInt32
valueWithBool:inContext:
valueWithDouble:inContext:
valueWithInt32:inContext:
valueWithUInt32:inContext:
NSDictionary Object object toDictionary valueWithNewObjectInContext:
NSArray Array object toArray valueWithNewArrayInContext:
NSDate Date object toDate
NSBlock Function object
id Wrapper object toObject
toObjectOfClass:
valueWithObject:inContext:
Class Constructor object

简单的数据类型的转换比较简单,不做过多说明,但是需要注意的是,OC对象与JS对象转换不是直接转换,两者面向对象的设计方式是不同的,前者基于class,后者基于prototype,JS中的对象返回后都可以视为键值对的集合,即对JS的对象以NSDictionary来进行访问.所以,NSDictionary传入JSContext后,也可以直接当做对象来调用.

然后是方法的转换,OC中的block可以直接传入JSContext中,作为JS的方法来使用,举例 :

context[@"log"] = ^() {
    NSArray *args = [JSContext currentArguments];
    for (id obj in args) {
        NSLog(@"%@",obj);
    }
};

JS方法的一个特点就是方法参数不固定,可以通过JSContextcurrentArguments方法获得完整的参数列表,通过currentThis获取当前调用该方法的对象.

OC的方法可以传入JSContext中调用,但是JS传入的方法JSValue却不能转换为block在OC中使用,不能把方法从JSValue中提取出来,但是可以通过方法- (JSValue *)callWithArguments:(NSArray *)arguments;来传递参数并调用JS方法.

也可以直接调用 - (JSValue *)invokeMethod:(NSString *)method withArguments:(NSArray *)arguments;方法来调用对象上的方法.

JSManagedValue

如果OC中持久保留了 JSValue,或者JS中持久保留了OC中传入的对象,但是两者的内存释放并没有绑定在一起.OC中使用自动计数ARC,JS使用垃圾回收机制GC.

OC中持有的JSValue,遵守OC的策略,如果在JS中释放了真实的对象,但OC中未释放,然后对其进行访问,就造成了错误访问.同理,JS持有OC对象,结果OC对象释放了,但是JS并不知道,继续去访问,也会发生错误.

所以有JSManagedValue类型来帮助管理内存,但只做到强持有JS对象,由OC代码来控制JS对象的释放 .

使用JSManagedValue时,调用addManagedReference:withOwner:,以通知虚拟机,该JS对象由owner对象负责管理。但是如果没有调用这个方法,JSManagedValue就像弱引用一样,会在JS垃圾回收后,自动将自己的Value属性设置为空(但是这种弱引用也不安全的,因为当我们原生调用JS时,调用过程中持有对象被回收了,会导致错误访问)。

JSExport

JSExport协议,为JSContext直接调用OC方法.举例说明:

// 供JS直接调用的属性和方法
@protocol PersonExport <JSExport>
@property (nonatomic,strong) NSString *name;
- (void)printName;
@end

@interface Person : NSObject <PersonExport>
@property (nonatomic,strong) NSString *realName;
@end
// 具体实现
@implementation Person
- (void)setName:(NSString *)name {
    self.realName = name;
}
- (NSString *)name {
    return self.realName;
}
- (void)printName {
	NSLog("%@",self.name);
}
@end

这样,这个Person类就有属性name和方法,直接供JS调用了:

Person *person = [[Person alloc] init];
context[@"person"] = person ;
[context evaluateScript:@"person.name = 'hello world';person.printName();"
 	withSourceURL:[NSURL URLWithString: @"main.js"]];
NSLog(@"person : %@" , person );

而如果没有设置JSExport时,这个name属性只能用以NSDictionary来传入,如果直接传入,调用person.name是无法访问的.而设置之后,就真的可以在JSContext中作为对象来使用了. 对于属性的传递,如果设置为readonly,那在JS中也就是只读属性,无法进行赋值.

对于方法,多参数的情况下,JavaScriptCore的转换方式将OC的方法中的每个部分都合并在一起,冒号后的字母大写,并移除冒号,如:

- (void)doFoo:(id)foo withBar:(id)bar; 
// 转换为:
doFooWithBar(foo, bar);

如果希望自定义命名,或者起一个较短的名字,可以使用JSExportAs(PropertyName, Selector),如:

@protocol LongArgs <JSExport>
 
JSExportAs(testArgumentTypes,
           - (NSString *)testArgumentTypesWithInt:(int)i double:(double)d 
                    boolean:(BOOL)b string:(NSString *)s number:(NSNumber *)n 
                    array:(NSArray *)a dictionary:(NSDictionary *)o
           );
 
@end

还有需要注意的是,只有直接继承JSExport的协议才能在JSContext中被访问,即只能继承一次,不能继承多次.源码中通过class_copyProtocolList来找到类所遵循的所有协议,然后在对每个协议使用protocol_copyProtocolList来查询是否继承了JSExport协议,如果继承了该协议,才会将方法给映射到JavaScript中.