您的位置:首页 > Web前端 > JavaScript

iOS7新JavaScriptCore框架入门介绍(二)

2016-02-15 09:03 681 查看
之前一篇的文章中已经简单入门了iOS7中新加的JavaScriptCore框架的基本用法,十分的简单方便而且高效,不过也仅限于数值型、布尔型、字符串、数组等这些基础类型。本文将扩展到更复杂的类型,介绍一下该强大的框架是如何让Objective-C对象和JavaScript对象进行直接互通的。

为了方便起见,以下所有代码中的JSContext对象都会添加如下的
log
方法和
eventHandler


JSContext *context = [[JSContext alloc] init];
context.exceptionHandler = ^(JSContext *con, JSValue *exception) {
NSLog(@"%@", exception);
con.exception = exception;
};

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



键值对编程—Dictionary

JSContext并不能让Objective-C和JavaScript的对象直接转换,毕竟两者的面向对象的设计方式是不同的:前者基于
class
,后者基于
prototype
。但所有的对象其实可以视为一组键值对的集合,所以JavaScript中的对象可以返回到Objective-C中当做
NSDictionary
类型进行访问。

JSValue *obj =[context evaluateScript:@"var jsObj = { number:7, name:'Ider' }; jsObj"];
NSLog(@"%@, %@", obj[@"name"], obj[@"number"]);
NSDictionary *dic = [obj toDictionary];
NSLog(@"%@, %@", dic[@"name"], dic[@"number"]);
//Output:
//  Ider, 7
//  Ider, 7


同样的,
NSDicionary
NSMutableDictionary
传入到JSContext之后也可以直接当对象来调用:

NSDictionary *dic = @{@"name": @"Ider", @"#":@(21)};
context[@"dic"] = dic;
[context evaluateScript:@"log(dic.name, dic['#'])"];
//OutPut:
//  Ider
//  21



语言穿梭机—JSExport协议

JavaScript可以脱离
prototype
继承完全用JSON来定义对象,但是Objective-C编程里可不能脱离类和继承了写代码。所以JavaScriptCore就提供了
JSExport
作为两种语言的互通协议。
JSExport
中没有约定任何的方法,连可选的(
@optional
)都没有,但是所有继承了该协议(
@protocol
)的协议(注意不是Objective-C的类(@interface))中定义的方法,都可以在
JSContext
中被使用。语言表述起来有点绕,还是用例子来说明会更明确一点。

@protocol PersonProtocol <JSExport>

@property (nonatomic, retain) NSDictionary *urls;
- (NSString *)fullName;

@end

@interface Person :NSObject <PersonProtocol>

@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;

@end;

@implementation Person

@synthesize firstName, lastName, urls;

- (NSString *)fullName {
return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
}

@end


在上边的代码中,定义了一个
PersonProtocol
,并让它继承了神秘的
JSExport
协议,在新定义的协议中约定
urls
属性和
fullName
方法。之后又定义了
Person
类,除了让它实现
PersonProtocol
外,还定义了firstName和lastName属性。而fullName方法返回的则是两部分名字的结合。

下边就来创建一个
Person
对象,然后传入到
JSContext
中并尝试使用JavaScript来访问和修改该对象。

// initialize person object
Person *person = [[Person alloc] init];
context[@"p"] = person;
person.firstName = @"Ider";
person.lastName = @"Zheng";
person.urls = @{@"site": @"http://www.iderzheng.com"};

// ok to get fullName
[context evaluateScript:@"log(p.fullName());"];
// cannot access firstName
[context evaluateScript:@"log(p.firstName);"];
// ok to access dictionary as object
[context evaluateScript:@"log('site:', p.urls.site, 'blog:', p.urls.blog);"];
// ok to change urls property
[context evaluateScript:@"p.urls = {blog:'http://blog.iderzheng.com'}"];
[context evaluateScript:@"log('-------AFTER CHANGE URLS-------')"];
[context evaluateScript:@"log('site:', p.urls.site, 'blog:', p.urls.blog);"];

// affect on Objective-C side as well
NSLog(@"%@", person.urls);

//Output:
//  Ider Zheng
//  undefined
//  undefined
//  site:
//  http://www.iderzheng.com //  blog:
//  undefined
//  -------AFTER CHANGE URLS-------
//  site:
//  undefined
//  blog:
//  http://blog.iderzheng.com //  {
//      blog = "http://blog.iderzheng.com";
//  }


从输出结果不难看出,当访问
firstName
lastName
的时候给出的结果是
undefined
,因为它们跟JavaScript没有
JSExport
的联系。但这并不影响从
fullName()
中正确得到两个属性的值。和之前说过的一样,对于
NSDictionary
类型的
urls
,可以在
JSContext
中当做对象使用,而且还可以正确地给
urls
赋予新的值,并反映到实际的Objective-C的
Person
对象上。

JSExport
不仅可以正确反映属性到JavaScript中,而且对属性的特性也会保证其正确,比如一个属性在协议中被声明成
readonly
,那么在JavaScript中也就只能读取属性值而不能赋予新的值。

对于多参数的方法,JavaScriptCore的转换方式将Objective-C的方法每个部分都合并在一起,冒号后的字母变为大写并移除冒号。比如下边协议中的方法,在JavaScript调用就是:
doFooWithBar(foo,
bar);


@protocol MultiArgs <JSExport>
- (void)doFoo:(id)foo withBar:(id)bar;
@end


如果希望方法在JavaScript中有一个比较短的名字,就需要用的JSExport.h中提供的宏:
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


比如上边定义的协议中的方法,在JavaScript就只要用
testArgumentTypes(i, d, b, s, n, a, dic);
来调用就可以了。

虽然JavaScriptCore框架还没有官方编程指南,但是在JSExport.h文件中对神秘协议的表述还是比较详细的,其中有一条是这样描述的:

By default no methods or properties of the Objective-C class will be exposed to JavaScript, however methods and properties may explicitly be exported. For each protocol that a class conforms to, if the protocol incorporates
the protocol JSExport, then the protocol will be interpreted as a list of methods and properties to be exported to JavaScript.

这里面有个incorporate一词值得推敲,经过验证只有直接继承了
JSExport
的自定义协议(
@protocol
)才能在
JSContext
中访问到。也就是说比如有其它的协议继承了上边的
PersonProtocol
,其中的定义的方法并不会被引入到
JSContext
中。从源码中也能看出JavaScriptCore框架会通过
class_copyProtocolList
方法找到类所遵循的协议,然后再对每个协议通过
protocol_copyProtocolList
检查它是否遵循JSExport协议进而将方法反映到JavaScript之中。


对已定义类扩展协议— class_addProtocol

对于自定义的Objective-C类,可以通过之前的方式自定义继承了
JSExport
的协议来实现与JavaScript的交互。对于已经定义好的系统类或者从外部引入的库类,她们都不会预先定义协议提供与JavaScript的交互的。好在Objective-C是可以在运行时实行对类性质的修改的

比如下边的例子,就是为
UITextField
添加了协议,让其能在JavaScript中可以直接访问
text
属性。该接口如下:

@protocol JSUITextFieldExport <JSExport>

@property(nonatomic,copy) NSString *text;

@end


之后在通过
class_addProtocol
为其添加上该协议:

- (void)viewDidLoad {
[super viewDidLoad];

textField.text = @"7";
class_addProtocol([UITextField class], @protocol(JSUITextFieldExport));
}


为一个
UIButton
添加如下的事件,其方法只要是将
textField
传入到
JSContext
中然后读取其
text
值,自增1后重新赋值:

- (IBAction)pressed:(id)sender {
JSContext *context = [[JSContext alloc] init];

context[@"textField"] = textField;

NSString *script = @"var num = parseInt(textField.text, 10);"
"++num;"
"textField.text = num;";
[context evaluateScript:script];
}


当运行点击UIButton时就会看到
UITextField
的值在不断增加,也证明了对于已定义的类,也可以在运行时添加神奇的
JSExport
协议让它们可以在Objective-C和JavaScript直接实现友好互通。






不同内存管理机制—Reference Counting vs. Garbage Collection

虽然Objetive-C和JavaScript都是面向对象的语言,而且它们都可以让程序员专心于业务逻辑,不用担心内存回收的问题。但是两者的内存回首机制全是不同的,Objective-C是基于引用计数,之后Xcode编译器又支持了自动引用计数(ARC,
Automatic Reference Counting);JavaScript则如同Java/C#那样用的是垃圾回收机制(GC,
Garbage Collection)。当两种不同的内存回收机制在同一个程序中被使用时就难免会产生冲突。

比如,在一个方法中创建了一个临时的Objective-C对象,然后将其加入到
JSContext
放在JavaScript中的变量中被使用。因为JavaScript中的变量有引用所以不会被释放回收,但是Objective-C上的对象可能在方法调用结束后,引用计数变0而被回收内存,因此JavaScript层面也会造成错误访问。

同样的,如果用
JSContext
创建了对象或者数组,返回
JSValue
到Objective-C,即使把
JSValue
变量
retain
下,但可能因为JavaScript中因为变量没有了引用而被释放内存,那么对应的
JSValue
也没有用了。

怎么在两种内存回收机制中处理好对象内存就成了问题。JavaScriptCore提供了
JSManagedValue
类型帮助开发人员更好地管理对象内存。

@interface JSManagedValue : NSObject

// Convenience method for creating JSManagedValues from JSValues.
+ (JSManagedValue *)managedValueWithValue:(JSValue *)value;

// Create a JSManagedValue.
- (id)initWithValue:(JSValue *)value;

// Get the JSValue to which this JSManagedValue refers. If the JavaScript value has been collected,
// this method returns nil.
- (JSValue *)value;

@end


在《iOS7新JavaScriptCore框架入门介绍》有提到
JSVirtualMachine
为整个JavaScriptCore的执行提供资源,所以当将一个
JSValue
转成
JSManagedValue
后,就可以添加到
JSVirtualMachine
中,这样在运行期间就可以保证在Objective-C和JavaScript两侧都可以正确访问对象而不会造成不必要的麻烦。

@interface JSVirtualMachine : NSObject

// Create a new JSVirtualMachine.
- (id)init;

// addManagedReference:withOwner and removeManagedReference:withOwner allow
// clients of JSVirtualMachine to make the JavaScript runtime aware of
// arbitrary external Objective-C object graphs. The runtime can then use
// this information to retain any JavaScript values that are referenced
// from somewhere in said object graph.
//
// For correct behavior clients must make their external object graphs
// reachable from within the JavaScript runtime. If an Objective-C object is
// reachable from within the JavaScript runtime, all managed references
// transitively reachable from it as recorded with
// addManagedReference:withOwner: will be scanned by the garbage collector.
//
- (void)addManagedReference:(id)object withOwner:(id)owner;
- (void)removeManagedReference:(id)object withOwner:(id)owner;

@end



了解更多更多—Source Code

对于iOS7提供JavaScriptCore已经介绍的差不多了,之前也提到这其实是一个开源的框架,所以如果想要在低版本的iOS上使用,也可以很容易地自行添加源码进行编译和使用。

阅读源码也可以更加了解JavaScriptCore是怎么实现的,在开发时候也可以注意到更多的细节避免错误的发生,想要阅读框架的源码可以在这里(源码1源码2源码3)。

文章中的代码和例子都比较简单,如果想了解更多JavaScriptCore的使用方法,在这里有详细的测试案例可以提供一些线索。不过经验证并不是所有的测试案例在iOS7中都会通过,这大概是测试案例所用的JavaScriptCore是为chromium实现的而iOS7是webkit吧。

References:

Steamclock Software – Apple’s
new Objective-C to Javascript Bridge
JavaScriptCore and iOS 7 » Big Nerd Ranch
BlogBig Nerd Ranch Blog
IOS7开发~JavaScriptCore (二) – 阿福的专栏 – 博客频道 –
CSDN.NET
API in trunk/Source/JavaScriptCore – WebKit
Objective-C
Runtime Reference
Automatic Reference
Counting vs. Garbage Collection – The Oxygene Language Wiki
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: