這個框架其實只是基於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/