这个框架其实只是基于webkit中以C/C++实现的JavaScriptCore的一个包装,在旧版本iOS开发中,不少开发者也会自行将webkit的库引入项目编译使用。不过虽然iOS7把它当成了标准库,惋惜目前,我尚未在Apple Developer中找到像以前文章中收集的那样的官方文档介绍这个框架的具体使用方法。前端
好在还能够在Xcode中找到头文件,并且里面的注释对每一个类和方法的功能写得还算清楚,再结合网上仅有的几篇文章的介绍,我也在此简单入门一下JavaScriptCore。更多细节还待开发过程当中发现。java
JavaScriptCore中的类
在项目中引入JavaScriptCore后,链到头文件中,除了大段的Copyright注释能够看到里面只要引入了5个文件,每一个文件里都定义跟文件名对应的类:android
- JSContext
- JSValue
- JSManagedValue
- JSVirtualMachine
- JSExport
虽然说代码中的注释介绍的也比较详细了,可是如同一图顶万言,对程序员来讲代码更有说服力。本文就先来讲说这些类相对比较好理解但又很经常使用的JSContext和JSValue以及它们方法的使用方式和效果。ios
JSContext和JSValue
JSVirtualMachine
为JavaScript的运行提供了底层资源,JSContext
就为其提供着运行环境,经过- (JSValue *)evaluateScript:(NSString *)script;
方法就能够执行一段JavaScript脚本,而且若是其中有方法、变量等信息都会被存储在其中以便在须要的时候使用。而JSContext的建立都是基于JSVirtualMachine
:- (id)initWithVirtualMachine:(JSVirtualMachine *)virtualMachine;
,若是是使用- (id)init;
进行初始化,那么在其内部会自动建立一个新的JSVirtualMachine
对象而后调用前边的初始化方法。git
JSValue
则能够说是JavaScript和Object-C之间互换的桥梁,它提供了多种方法能够方便地把JavaScript数据类型转换成Objective-C,或者是转换过去。其一一对应方式可见下表:程序员
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 |
基本类型转换
先看个简单的例子:github
-
JSContext *context = [[JSContext alloc] init];
-
JSValue *jsVal = [context evaluateScript:@"21+7"];
-
int iVal = [jsVal toInt32];
-
NSLog (@"JSValue: %@, int: %d", jsVal, iVal);
-
-
//Output:
-
// JSValue: 28, int: 28
很简单吧,还能够存一个JavaScript变量在JSContext
中,而后经过下标来获取出来。而对于Array
或者Object
类型,JSValue
也能够经过下标直接取值和赋值。web
-
JSContext *context = [[JSContext alloc] init];
-
[context evaluateScript:@"var arr = [21, 7 , 'iderzheng.com'];"];
-
JSValue *jsArr = context[@"arr"]; // Get array from JSContext
-
-
NSLog (@"JS Array: %@; Length: %@", jsArr, jsArr[@"length"]);
-
jsArr [1] = @"blog"; // Use JSValue as array
-
jsArr [7] = @7;
-
-
NSLog (@"JS Array: %@; Length: %d", jsArr, [jsArr[@"length"] toInt32]);
-
-
NSArray *nsArr = [jsArr toArray];
-
NSLog (@"NSArray: %@", nsArr);
-
-
//Output:
-
// JS Array: 21,7,iderzheng.com Length: 3
-
// JS Array: 21,blog,iderzheng.com,,,,,7 Length: 8
-
// NSArray: (
-
// 21,
-
// blog,
-
// "iderzheng.com",
-
// "<null>",
-
// "<null>",
-
// "<null>",
-
// "<null>",
-
// 7
-
// )
经过输出结果很容易看出代码成功把数据从Objective-C赋到了JavaScript数组上,并且JSValue
是遵循JavaScript的数组特性:无下标越位,自动延展数组大小。而且经过JSValue
还能够获取JavaScript对象上的属性,好比例子中经过"length"
就获取到了JavaScript数组的长度。在转成NSArray
的时候,全部的信息也都正确转换了过去。
方法的转换
各类数据类型能够转换,Objective-C的Block也能够传入JSContext中当作JavaScript的方法使用。好比在前端开发中经常使用的log
方法,虽然JavaScritpCore没有自带(毕竟不是在网页上运行的,天然不会有window、document、console这些类了),仍然能够定义一个Block方法来调用NSLog来模拟:
-
JSContext *context = [[JSContext alloc] init];
-
context [@"log"] = ^() {
-
NSLog (@"+++++++Begin Log+++++++");
-
-
NSArray *args = [JSContext currentArguments];
-
for (JSValue *jsVal in args) {
-
NSLog (@"%@", jsVal);
-
}
-
-
JSValue *this = [JSContext currentThis];
-
NSLog (@"this: %@",this);
-
NSLog (@"-------End Log-------");
-
};
-
-
[context evaluateScript:@"log('ider', [7, 21], { hello:'world', js:100 });"];
-
-
//Output:
-
// +++++++Begin Log+++++++
-
// ider
-
// 7,21
-
// [object Object]
-
// this: [object GlobalObject]
-
// -------End Log-------
经过Block成功的在JavaScript调用方法回到了Objective-C,并且依然遵循JavaScript方法的各类特色,好比方法参数不固定。也由于这样,JSContext
提供了类方法来获取参数列表(+ (JSContext *) currentArguments;
)和当前调用该方法的对象(+ (JSValue *)currentThis
)。对于"this"
,输出的内容是GlobalObject
,这也是JSContext
对象方法- (JSValue *)globalObject;
所返回的内容。由于咱们知道在JavaScript里,全部全局变量和方法其实都是一个全局变量的属性,在浏览器中是window,在JavaScriptCore是什么就不得而知了。
Block能够传入JSContext
做方法,可是JSValue
没有toBlock
方法来把JavaScript方法变成Block在Objetive-C中使用。毕竟Block的参数个数和类型已经返回类型都是固定的。虽然不能把方法提取出来,可是JSValue
提供了- (JSValue *)callWithArguments:(NSArray *)arguments;
方法能够反过来将参数传进去来调用方法。
-
JSContext *context = [[JSContext alloc] init];
-
[context evaluateScript:@"function add(a, b) { return a + b; }"];
-
JSValue *add = context[@"add"];
-
NSLog (@"Func: %@", add);
-
-
JSValue *sum = [add callWithArguments:@[@(7), @(21)]];
-
NSLog (@"Sum: %d",[sum toInt32]);
-
//OutPut:
-
// Func: function add(a, b) { return a + b; }
-
// Sum: 28
JSValue
还提供
- (JSValue *)invokeMethod:(NSString *)method withArguments:(NSArray *)arguments;
让咱们能够直接简单地调用对象上的方法。只是若是定义的方法是全局函数,那么很显然应该在
JSContext
的
globalObject
对象上调用该方法;若是是某JavaScript对象上的方法,就应该用相应的
JSValue
对象调用。
异常处理
Objective-C的异常会在运行时被Xcode捕获,而在JSContext
中执行的JavaScript若是出现异常,只会被JSContext
捕获并存储在exception
属性上,而不会向外抛出。时时刻刻检查JSContext
对象的exception
是否不为nil
显然是不合适,更合理的方式是给JSContext
对象设置exceptionHandler
,它接受的是^(JSContext *context, JSValue *exceptionValue)
形式的Block。其默认值就是将传入的exceptionValue
赋给传入的context
的exception
属性:
-
^(JSContext *context, JSValue *exceptionValue) {
-
context.exception = exceptionValue;
-
};
咱们也能够给exceptionHandler
赋予新的Block以便在JavaScript运行发生异常的时候咱们能够当即知道:
-
JSContext *context = [[JSContext alloc] init];
-
context.exceptionHandler = ^(JSContext *con, JSValue *exception) {
-
NSLog (@"%@", exception);
-
con.exception = exception;
-
};
-
-
[context evaluateScript:@"ider.zheng = 21"];
-
-
//Output:
-
// ReferenceError: Can't find variable: ider
使用Block的注意事项
从以前的例子和介绍应该有体会到Block在JavaScriptCore中起到的强大做用,它在JavaScript和Objective-C之间的转换 创建起更多的桥梁,让互通更方便。可是要注意的是不管是把Block传给JSContext
对象让其变成JavaScript方法,仍是把它赋给exceptionHandler
属性,在Block内都不要直接使用其外部定义的JSContext
对象或者JSValue
,应该将其当作参数传入到Block中,或者经过JSContext
的类方法+ (JSContext *)currentContext;
来得到。不然会形成循环引用使得内存没法被正确释放。
好比上边自定义异常处理方法,就是赋给传入JSContext
对象con
,而不是其外建立的context
对象,虽然它们实际上是同一个对象。这是由于Block会对内部使用的在外部定义建立的对象作强引用,而JSContext
也会对被赋予的Block作强引用,这样它们之间就造成了循环引用(Circular Reference)使得内存没法正常释放。
对于JSValue
也不能直接从外部引用到Block中,由于每一个JSValue
上都有JSContext
的引用 (@property(readonly, retain) JSContext *context;
),JSContext
再引用Block一样也会造成引用循环。
前面十分的简单方便并且高效,不过也仅限于数值型、布尔型、字符串、数组等这些基础类型。本文将扩展到更复杂的类型,介绍一下该强大的框架是如何让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
-
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
- http://www.oschina.net/translate/how-to-choose-a-javascript-engine-for-ios-and-android-apps?cmp
感谢:http://blog.iderzheng.com/introduction-to-ios7-javascriptcore-framework/