前陣子,Apple正式發布了新的iOS 7系統,最大最直觀的改變在於界面變得小清新范了,我也提到《iOS,你真的越來越像Android了》。不過對於移動開發者來說,除了要適應Xcode 5,最應該關注的還是iOS 7在開發接口的上的變化。概覽Apple提供的官方文檔《What’s New in iOS》,最最讓我欣喜的是iOS 7中加入了JavaScriptCore框架。該框架讓Objective-C和JavaScript代碼直接的交互變得更加的簡單方便。
這個框架其實只是基於webkit中以C/C++實現的JavaScriptCore的一個包裝,在舊版本iOS開發中,很多開發者也會自行將webkit的庫引入項目編譯使用。不過雖然iOS7把它當成了標准庫,可惜目前,我還沒有在Apple Developer中找到像之前文章中收集的那樣的官方文檔介紹這個框架的具體使用方法。
好在還可以在Xcode中找到頭文件,而且里面的注釋對每個類和方法的功能寫得還算清楚,再結合網上僅有的幾篇文章的介紹,我也在此簡單入門一下JavaScriptCore框架在基本數據類型上的使用。在另一篇《JavaScriptCore框架在iOS7中的對象交互和管理》中,我更深入地介紹該框架在對象上的使用。
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,或者是轉換過去。其一一對應方式可見下表:
Objective-C | JavaScript | JSValue Convert | JSValue Constructor |
---|---|---|---|
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 |
基本類型轉換
先看個簡單的例子:
1 JSContext *context = [[JSContext alloc] init]; 2 JSValue *jsVal = [context evaluateScript:@"21+7"]; 3 int iVal = [jsVal toInt32]; 4 NSLog(@"JSValue: %@, int: %d", jsVal, iVal); 5 6 //Output: 7 // JSValue: 28, int: 28
很簡單吧,還可以存一個JavaScript變量在JSContext
中,然后通過下標來獲取出來。而對於Array
或者Object
類型,JSValue
也可以通過下標直接取值和賦值。
1 JSContext *context = [[JSContext alloc] init]; 2 [context evaluateScript:@"var arr = [21, 7 , 'iderzheng.com'];"]; 3 JSValue *jsArr = context[@"arr"]; // Get array from JSContext 4 5 NSLog(@"JS Array: %@; Length: %@", jsArr, jsArr[@"length"]); 6 jsArr[1] = @"blog"; // Use JSValue as array 7 jsArr[7] = @7; 8 9 NSLog(@"JS Array: %@; Length: %d", jsArr, [jsArr[@"length"] toInt32]); 10 11 NSArray *nsArr = [jsArr toArray]; 12 NSLog(@"NSArray: %@", nsArr); 13 14 //Output: 15 // JS Array: 21,7,iderzheng.com Length: 3 16 // JS Array: 21,blog,iderzheng.com,,,,,7 Length: 8 17 // NSArray: ( 18 // 21, 19 // blog, 20 // "iderzheng.com", 21 // "<null>", 22 // "<null>", 23 // "<null>", 24 // "<null>", 25 // 7 26 // )
通過輸出結果很容易看出代碼成功把數據從Objective-C賦到了JavaScript數組上,而且JSValue
是遵循JavaScript的數組特性:無下標越位,自動延展數組大小。並且通過JSValue
還可以獲取JavaScript對象上的屬性,比如例子中通過"length"
就獲取到了JavaScript數組的長度。在轉成NSArray
的時候,所有的信息也都正確轉換了過去。
方法的轉換
各種數據類型可以轉換,Objective-C的Block也可以傳入JSContext
中當做JavaScript的方法使用。比如在前端開發中常用的log
方法,雖然JavaScritpCore沒有自帶(畢竟不是在網頁上運行的,自然不會有window、document、console這些類了),仍然可以定義一個Block方法來調用NSLog來模擬:
1 JSContext *context = [[JSContext alloc] init]; 2 context[@"log"] = ^() { 3 NSLog(@"+++++++Begin Log+++++++"); 4 5 NSArray *args = [JSContext currentArguments]; 6 for (JSValue *jsVal in args) { 7 NSLog(@"%@", jsVal); 8 } 9 10 JSValue *this = [JSContext currentThis]; 11 NSLog(@"this: %@",this); 12 NSLog(@"-------End Log-------"); 13 }; 14 15 [context evaluateScript:@"log('ider', [7, 21], { hello:'world', js:100 });"]; 16 17 //Output: 18 // +++++++Begin Log+++++++ 19 // ider 20 // 7,21 21 // [object Object] 22 // this: [object GlobalObject] 23 // -------End Log-------
通過Block成功的在JavaScript調用方法回到了Objective-C,而且依然遵循JavaScript方法的各種特點,比如方法參數不固定。也因為這樣,JSContext
提供了類方法來獲取參數列表(+ (JSContext *)currentContext;
)和當前調用該方法的對象(+ (JSValue *)currentThis
)。對於"this"
,輸出的內容是GlobalObject
,這也是JSContext
對象方法- (JSValue *)globalObject;
所返回的內容。因為我們知道在JavaScript里,所有全局變量和方法其實都是一個全局變量的屬性,在瀏覽器中是window,在JavaScriptCore是什么就不得而知了。
Block可以傳入JSContext
作方法,但是JSValue
沒有toBlock
方法來把JavaScript方法變成Block在Objetive-C中使用。畢竟Block的參數個數和類型已經返回類型都是固定的。雖然不能把方法提取出來,但是JSValue
提供了- (JSValue *)callWithArguments:(NSArray *)arguments;
方法可以反過來將參數傳進去來調用方法。
1 JSContext *context = [[JSContext alloc] init]; 2 [context evaluateScript:@"function add(a, b) { return a + b; }"]; 3 JSValue *add = context[@"add"]; 4 NSLog(@"Func: %@", add); 5 6 JSValue *sum = [add callWithArguments:@[@(7), @(21)]]; 7 NSLog(@"Sum: %d",[sum toInt32]); 8 //OutPut: 9 // Func: function add(a, b) { return a + b; } 10 // 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
屬性:
1 ^(JSContext *context, JSValue *exceptionValue) { 2 context.exception = exceptionValue; 3 };
我們也可以給exceptionHandler
賦予新的Block以便在JavaScript運行發生異常的時候我們可以立即知道:
1 JSContext *context = [[JSContext alloc] init]; 2 context.exceptionHandler = ^(JSContext *con, JSValue *exception) { 3 NSLog(@"%@", exception); 4 con.exception = exception; 5 }; 6 7 [context evaluateScript:@"ider.zheng = 21"]; 8 9 //Output: 10 // 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同樣也會形成引用循環。
歡迎繼續閱讀《JavaScriptCore框架在iOS7中的對象交互和管理》了解更多JavaScriptCore框架