曾经想要oc调用js代码通常是通过uiwebview的stringByEvaluatingJavaScriptFromString方法,或是直接使用JavaScriptCore引擎,比如https://github.com/jfahrenkrug/AddressBookSpy。
可是,Apple正式公布了新的iOS 7系统,引入了JavaScriptCore.framework ,最大最直观的改变在于界面变得小清新范了,我也提到。只是对于移动开发人员来说,除了要适应Xcode 5,最应该关注的还是iOS 7在开发接口的上的变化。概览Apple提供的官方文档,最最让我欣喜的是iOS 7中增加了JavaScriptCore框架。该框架让Objective-C和JavaScript代码直接的交互变得更加的简单方便。
这个框架事实上仅仅是基于中以C/C++实现的JavaScriptCore的一个包装,在旧版本号iOS开发中,非常多开发人员也会自行将webkit的库引入项目编译使用。只是尽管iOS7把它当成了标准库,可惜眼下,我还没有在Apple Developer中找到像那样的官方文档介绍这个框架的详细用法。
好在还能够在Xcode中找到头文件,并且里面的凝视对每一个类和方法的功能写得还算清楚,再结合网上仅有的几篇文章的介绍,我也在此简单入门一下JavaScriptCore。很多其它细节还待开发过程中发现。
JavaScriptCore中的类
在项目中引入JavaScriptCore后,链到头文件中,除了大段的Copyright凝视能够看到里面仅仅要引入了5个文件,每一个文件中都定义跟文件名称相应的类:
- JSContext
- JSValue
- JSManagedValue
- JSVirtualMachine
- JSExport
虽说代码中的凝视介绍的也比較具体了,可是如同一图顶万言,对程序猿来说代码更有说服力。本文就先来说说这些类相对照较好理解但又非经常常使用的JSContext和JSValue以及它们方法的使用方式和效果。
JSContext和JSValue
JSVirtualMachine
为JavaScript的执行提供了底层资源,JSContext
就为其提供着执行环境,通过- (JSValue *)evaluateScript:(NSString *)script;
方法就能够运行一段JavaScript脚本,而且假设当中有方法、变量等信息都会被存储在当中以便在须要的时候使用。而JSContext的创建都是基于JSVirtualMachine
:- (id)initWithVirtualMachine:(JSVirtualMachine *)virtualMachine;
,假设是使用- (id)init;
进行初始化,那么在其内部会自己主动创建一个新的JSVirtualMachine
对象然后调用前边的初始化方法。
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 |
基本类型转换
先看个简单的样例:
- 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
非常easy吧,还能够存一个JavaScript变量在JSContext
中,然后通过下标来获取出来。而对于Array
或者Object
类型,JSValue
也能够通过下标直接取值和赋值。
- 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 ] );
-
- *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
- // )
通过输出结果非常easy看出代码成功把数据从Objective-C赋到了JavaScript数组上,并且JSValue
是遵循JavaScript的数组特性:无下标越位,自己主动延展数组大小。并且通过JSValue
还能够获取JavaScript对象上的属性,比方样例中通过"length"
就获取到了JavaScript数组的长度。在转成NSArray
的时候,全部的信息也都正确转换了过去。
方法的转换
各种数据类型能够转换,Objective-C的也能够传入JSContext中当做JavaScript的方法使用。比方在前端开发中经常使用的方法,尽管JavaScritpCore没有自带(毕竟不是在网页上执行的,自然不会有window、document、console这些类了),仍然能够定义一个Block方法来调用NSLog来模拟:
- JSContext *context = [ [JSContext alloc ] init ];
- context [ @ "log" ] = ^ ( ) {
- NSLog ( @ "+++++++Begin Log+++++++" );
-
- *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里,全部全局变量和方法事实上都是一个全局变量的属性,在浏览器中是,在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做强引用,这样它们之间就形成了循环引用()使得内存无法正常释放。
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" ] = ^ ( ) {
- *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" ] );
- *dic = [obj toDictionary ];
- NSLog ( @ "%@, %@", dic [ @ "name" ], dic [ @ "number" ] );
- //Output:
- // Ider, 7
- // Ider, 7
相同的,NSDicionary
和NSMutableDictionary
传入到JSContext之后也能够直接当对象来调用:
- *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
中没有约定不论什么的方法,连可选的()都没有,可是全部继承了该协议(@protocol
)的协议(注意不是Objective-C的类(@interface))中定义的方法,都能够在JSContext
中被使用。语言表述起来有点绕,还是用样例来说明会更明白一点。
- @protocol PersonProtocol <JSExport>
-
- @property (nonatomic, retain ) *urls;
- - ( * )fullName;
-
- @end
-
- @interface Person : <PersonProtocol>
-
- @property (nonatomic, copy ) *firstName;
- @property (nonatomic, copy ) *lastName;
-
- @end;
-
- @implementation Person
-
- @synthesize firstName, lastName, urls;
-
- - ( * )fullName {
- return [ 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,
- - ( * )testArgumentTypesWithInt : ( int )i double : ( double )d
- boolean : ( BOOL )b string : ( * )s number : ( * )n
- array : ( * )a dictionary : ( * )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框架会通过方法找到类所遵循的协议,然后再对每一个协议通过检查它是否遵循JSExport协议进而将方法反映到JavaScript之中。
对已定义类扩展协议— class_addProtocol
对于自己定义的Objective-C类,能够通过之前的方式自己定义继承了JSExport
的协议来实现与JavaScript的交互。对于已经定义好的系统类或者从外部引入的库类,她们都不会预先定义协议提供与JavaScript的交互的。好在。
比方下边的样例,就是为UITextField
加入了协议,让其能在JavaScript中能够直接訪问text
属性。该接口例如以下:
- @protocol JSUITextFieldExport <JSExport>
-
- @property (nonatomic,copy ) *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;
-
- *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编译器又支持了;JavaScript则如同Java/C#那样用的是。当两种不同的内存回收机制在同一个程序中被使用时就难免会产生冲突。
比方,在一个方法中创建了一个暂时的Objective-C对象,然后将其增加到JSContext
放在JavaScript中的变量中被使用。由于JavaScript中的变量有引用所以不会被释放回收,可是Objective-C上的对象可能在方法调用结束后,引用计数变0而被回收内存,因此JavaScript层面也会造成错误訪问。
相同的,假设用JSContext
创建了对象或者数组,返回JSValue
到Objective-C,即使把JSValue
变量retain
下,但可能由于JavaScript中由于变量没有了引用而被释放内存,那么相应的JSValue
也没实用了。
怎么在两种内存回收机制中处理好对象内存就成了问题。JavaScriptCore提供了JSManagedValue
类型帮助开发者更好地管理对象内存。
- @interface JSManagedValue :
-
- // 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
在《》有提到JSVirtualMachine
为整个JavaScriptCore的执行提供资源,所以当将一个JSValue
转成JSManagedValue
后,就能够加入到JSVirtualMachine
中,这样在执行期间就能够保证在Objective-C和JavaScript两側都能够正确訪问对象而不会造成不必要的麻烦。
- @interface JSVirtualMachine :
-
- // 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