之前一篇的文章中已經簡單入門了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吧