以前一篇的文章中已經簡單入門了iOS7中新加的JavaScriptCore框架的基本用法,十分的簡單方便並且高效,不過也僅限於數值型、布爾型、字符串、數組等這些基礎類型。本文將擴展到更復雜的類型,介紹一下該強大的框架是如何讓Objective-C對象和JavaScript對象進行直接互通的。javascript
爲了方便起見,如下全部代碼中的JSContext對象都會添加以下的log
方法和eventHandler
:html
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);}};
JSContext並不能讓Objective-C和JavaScript的對象直接轉換,畢竟二者的面向對象的設計方式是不一樣的:前者基於class
,後者基於prototype
。但全部的對象其實能夠視爲一組鍵值對的集合,因此JavaScript中的對象能夠返回到Objective-C中當作NSDictionary
類型進行訪問。java
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以後也能夠直接當對象來調用:ios
NSDictionary *dic = @{@"name": @"Ider", @"#":@(21)};context[@"dic"] = dic;[context evaluateScript:@"log(dic.name, dic['#'])"];//OutPut:// Ider// 21
JavaScript能夠脫離prototype
繼承徹底用JSON來定義對象,可是Objective-C編程裏可不能脫離類和繼承了寫代碼。因此JavaScriptCore就提供了JSExport
做爲兩種語言的互通協議。JSExport
中沒有約定任何的方法,連可選的(@optional
)都沒有,可是全部繼承了該協議(@protocol
)的協議(注意不是Objective-C的類(@interface))中定義的方法,均可以在JSContext
中被使用。語言表述起來有點繞,仍是用例子來講明會更明確一點。git
@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來訪問和修改該對象。web
// initialize person objectPerson *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 wellNSLog(@"%@", 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
對象上。objective-c
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)dboolean:(BOOL)b string:(NSString *)s number:(NSNumber *)narray:(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之中。
對於自定義的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直接實現友好互通。
雖然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
對於iOS7提供JavaScriptCore已經介紹的差很少了,以前也提到這實際上是一個開源的框架,因此若是想要在低版本的iOS上使用,也能夠很容易地自行添加源碼進行編譯和使用。
閱讀源碼也能夠更加了解JavaScriptCore是怎麼實現的,在開發時候也能夠注意到更多的細節避免錯誤的發生,想要閱讀框架的源碼能夠在這裏(源碼1,源碼2,源碼3)。
文章中的代碼和例子都比較簡單,若是想了解更多JavaScriptCore的使用方法,在這裏有詳細的測試案例能夠提供一些線索。不過經驗證並非全部的測試案例在iOS7中都會經過,這大概是測試案例所用的JavaScriptCore是爲chromium實現的而iOS7是webkit吧。
References: