注:JavaScriptCore API也可以用Swift來調用,本文用Objective-C來介紹。
在iOS7之前,原生應用和Web應用之間很難通信。如果你想在iOS設備上渲染HTML或者運行JavaScript,你不得不使用UIWebView。iOS7引入了JavaScriptCore,功能更強大,使用更簡單。
JavaScriptCore介紹
JavaScriptCore是封裝了JavaScript和Objective-C橋接的Objective-C API,只要用很少的代碼,就可以做到JavaScript調用Objective-C,或者Objective-C調用JavaScript。
在之前的iOS版本,你只能通過向UIWebView發(fā)送stringByEvaluatingJavaScriptFromString:消息來執(zhí)行一段JavaScript腳本。并且如果想用JavaScript調用Objective-C,必須打開一個自定義的URL(例如:foo://),然后在UIWebView的delegate方法webView:shouldStartLoadWithRequest:navigationType中進行處理。
然而現(xiàn)在可以利用JavaScriptCore的先進功能了,它可以:
運行JavaScript腳本而不需要依賴UIWebView
使用現(xiàn)代Objective-C的語法(例如Blocks和下標)
在Objective-C和JavaScript之間無縫的傳遞值或者對象
創(chuàng)建混合對象(原生對象可以將JavaScript值或函數(shù)作為一個屬性)
使用Objective-C和JavaScript結合開發(fā)的好處:
快速的開發(fā)和制作原型:
如果某塊區(qū)域的業(yè)務需求變化的非常頻繁,那么可以用JavaScript來開發(fā)和制作原型,這比Objective-C效率更高。
團隊職責劃分:
這部分參考原文吧
Since JavaScript is much easier to learn and use than Objective-C (especially if you develop a nice JavaScript sandbox), it can be handy to have one team of developers responsible for the Objective-C “engine/framework”, and another team of developers write the JavaScript that uses the “engine/framework”. Even non-developers can write JavaScript, so it’s great if you want to get designers or other folks on the team involved in certain areas of the app.
JavaScript是解釋型語言:
JavaScript是解釋運行的,你可以實時的修改JavaScript代碼并立即看到結果。
邏輯寫一次,多平臺運行:
可以把邏輯用JavaScript實現(xiàn),iOS端和Android端都可以調用
JavaScriptCore概述
JSValue: 代表一個JavaScript實體,一個JSValue可以表示很多JavaScript原始類型例如boolean, integers, doubles,甚至包括對象和函數(shù)。
JSManagedValue: 本質上是一個JSValue,但是可以處理內存管理中的一些特殊情形,它能幫助引用技術和垃圾回收這兩種內存管理機制之間進行正確的轉換。
JSContext: 代表JavaScript的運行環(huán)境,你需要用JSContext來執(zhí)行JavaScript代碼。所有的JSValue都是捆綁在一個JSContext上的。
JSExport: 這是一個協(xié)議,可以用這個協(xié)議來將原生對象導出給JavaScript,這樣原生對象的屬性或方法就成為了JavaScript的屬性或方法,非常神奇。
JSVirtualMachine: 代表一個對象空間,擁有自己的堆結構和垃圾回收機制。大部分情況下不需要和它直接交互,除非要處理一些特殊的多線程或者內存管理問題。
JSContext / JSValue
JSVirtualMachine為JavaScript的運行提供了底層資源,JSContext為JavaScript提供運行環(huán)境,通過
- (JSValue *)evaluateScript:(NSString *)script;
方法就可以執(zhí)行一段JavaScript腳本,并且如果其中有方法、變量等信息都會被存儲在其中以便在需要的時候使用。 而JSContext的創(chuàng)建都是基于JSVirtualMachine:
- (id)initWithVirtualMachine:(JSVirtualMachine *)virtualMachine;
如果是使用- (id)init;進行初始化,那么在其內部會自動創(chuàng)建一個新的JSVirtualMachine對象然后調用前邊的初始化方法。
創(chuàng)建一個 JSContext 后,可以很容易地運行 JavaScript 代碼來創(chuàng)建變量,做計算,甚至定義方法:
JSContext *context = [[JSContext alloc] init];[context evaluateScript:@"var num = 5 + 5"];[context evaluateScript:@"var names = ['Grace', 'Ada', 'Margaret']"];[context evaluateScript:@"var triple = function(value) { return value * 3 }"];JSValue *tripleNum = [context evaluateScript:@"triple(num)"];
任何出自 JSContext 的值都被可以被包裹在一個 JSValue 對象中,JSValue 包裝了每一個可能的 JavaScript 值:字符串和數(shù)字;數(shù)組、對象和方法;甚至錯誤和特殊的 JavaScript 值諸如 null 和 undefined。
可以對JSValue調用toString、toBool、toDouble、toArray等等方法把它轉換成合適的Objective-C值或對象。
Objective-C調用JavaScript
例如有一個"Hello.js"文件內容如下:
functionprintHello() {
}
在Objective-C中調用printHello方法:
NSString *scriptPath = [[NSBundle mainBundle] pathForResource:@"hello" ofType:@"js"];NSString *scriptString = [NSString stringWithContentsOfFile:scriptPath encoding:NSUTF8StringEncoding error:nil];JSContext *context = [[JSContext alloc] init];[context evaluateScript:scriptString];JSValue *function =self.context[@"printHello"];
[function callWithArguments:@[]];
分析以上代碼:
首先初始化了一個JSContext,并執(zhí)行JavaScript腳本,此時printHello函數(shù)并沒有被調用,只是被讀取到了這個context中。
然后從context中取出對printHello函數(shù)的引用,并保存到一個JSValue中。
注意這里,從JSContext中取出一個JavaScript實體(值、函數(shù)、對象),和將一個實體保存到JSContext中,語法均與NSDictionary的取值存值類似,非常簡單。
最后如果JSValue是一個JavaScript函數(shù),可以用callWithArguments來調用,參數(shù)是一個數(shù)組,如果沒有參數(shù)則傳入空數(shù)組@[]。
JavaScript調用Objective-C
還是上面的例子,將"hello.js"的內容改為:
functionprintHello() {? ? print("Hello, World!");
}
這里的print函數(shù)用Objective-C代碼來實現(xiàn)
NSString *scriptPath = [[NSBundle mainBundle] pathForResource:@"hello" ofType:@"js"];
NSString *scriptString = [NSString stringWithContentsOfFile:scriptPath encoding:NSUTF8StringEncoding error:nil];
JSContext *context = [[JSContext alloc] init];
[context evaluateScript:scriptString];
self.context[@"print"] = ^(NSString *text) {
NSLog(@"%@", text");
};
JSValue *function = self.context[@"printHello"];
[function callWithArguments:@[]];
這里將一個Block以"print"為名傳遞給JavaScript上下文,JavaScript中調用print函數(shù)就可以執(zhí)行這個Objective-C Block。
注意這里JavaScript中的字符串可以無縫的橋接為NSString,實參"Hello, World!"被傳遞給了NSString類型的text形參。
異常處理
當JavaScript運行時出現(xiàn)異常,會回調JSContext的exceptionHandler中設置的Block
context.exceptionHandler = ^(JSContext *context, JSValue *exception) {NSLog(@"JS Error: %@", exception);};[context evaluateScript:@"function multiply(value1, value2) { return value1 * value2 "];// 此時會打印Log "JS Error: SyntaxError: Unexpected end of script"
JSExport
JSExport是一個協(xié)議,可以讓原生類的屬性或方法稱為JavaScript的屬性或方法。
看下面的例子:
@protocolItemExport @property (strong,nonatomic)NSString *name;@property (strong,nonatomic)NSString *description;@end@interfaceItem :NSObject @property (strong,nonatomic)NSString *name;@property (strong,nonatomic)NSString *description;@end
注意Item類不去直接符合JSExport,而是符合一個自己的協(xié)議,這個協(xié)議去繼承JSExport協(xié)議。
例如有如下JavaScript代碼
functionItem(name, description) {this.name = name;this.description = description;}var items = [];functionaddItem(item) {
items.push(item);
}
可以在Objective-C中把Item對象傳遞給addItem函數(shù)
Item *item = [[Item alloc] init];item.name =@"itemName";item.description =@"itemDescription";JSValue *function = context[@"addItem"];
[function callWithArguments:@[item]];
或者把Item類導出到JavaScript環(huán)境,等待稍后使用
[self.context setObject:Item.self forKeyedSubscript:@"Item"];
內存管理陷阱
Objective-C的內存管理機制是引用計數(shù),JavaScript的內存管理機制是垃圾回收。在大部分情況下,JavaScriptCore能做到在這兩種內存管理機制之間無縫無錯轉換,但也有少數(shù)情況需要特別注意。
在block內捕獲JSContext
Block會為默認為所有被它捕獲的對象創(chuàng)建一個強引用。JSContext為它管理的所有JSValue也都擁有一個強引用。并
且,JSValue會為它保存的值和它所在的Context都維持一個強引用。這樣JSContext和JSValue看上去是循環(huán)引用的,然而并不會,
垃圾回收機制會打破這個循環(huán)引用。
看下面的例子:
self.context[@"getVersion"] = ^{NSString *versionString = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"];? ? versionString = [@"version " stringByAppendingString:versionString];? ? JSContext *context = [JSContext currentContext];// 這里不要用self.context? ? JSValue *version = [JSValue valueWithObject:versionString inContext:context];return version;
};
使用[JSContext currentContext]而不是self.context來在block中使用JSContext,來防止循環(huán)引用。
JSManagedValue
當把一個JavaScript值保存到一個本地實例變量上時,需要尤其注意內存管理陷阱。 用實例變量保存一個JSValue非常容易引起循環(huán)引用。
看以下下例子,自定義一個UIAlertView,當點擊按鈕時調用一個JavaScript函數(shù):
#import#import@interfaceMyAlertView :UIAlertView- (id)initWithTitle:(NSString *)title? ? ? ? ? ? message:(NSString *)message? ? ? ? ? ? success:(JSValue *)successHandler? ? ? ? ? ? failure:(JSValue *)failureHandler? ? ? ? ? ? context:(JSContext *)context;@end
按照一般自定義AlertView的實現(xiàn)方法,MyAlertView需要持有successHandler,failureHandler這兩個JSValue對象
向JavaScript環(huán)境注入一個function
self.context[@"presentNativeAlert"] = ^(NSString *title,NSString *message,
JSValue *success,
JSValue *failure) {
JSContext *context = [JSContext currentContext];
MyAlertView *alertView = [[MyAlertView alloc] initWithTitle:title
message:message
success:success
failure:failure
context:context];
[alertView show];
};
因為JavaScript環(huán)境中都是“強引用”(相對Objective-C的概念來說)的,這時JSContext強引用了一個
presentNativeAlert函數(shù),這個函數(shù)中又強引用了MyAlertView
等于說JSContext強引用了MyAlertView,而MyAlertView為了持有兩個回調強引用了successHandler和
failureHandler這兩個JSValue,這樣MyAlertView和JavaScript環(huán)境互相引用了。
所以蘋果提供了一個JSMagagedValue類來解決這個問題。
看MyAlertView.m的正確實現(xiàn):
#import"MyAlertView.h"@interfaceXorkAlertView() @property (strong,nonatomic) JSContext *ctxt;@property (strong,nonatomic) JSMagagedValue *successHandler;@property (strong,nonatomic) JSMagagedValue *failureHandler;@end@implementationMyAlertView- (id)initWithTitle:(NSString *)title? ? ? ? ? ? message:(NSString *)message? ? ? ? ? ? success:(JSValue *)successHandler? ? ? ? ? ? failure:(JSValue *)failureHandler? ? ? ? ? ? context:(JSContext *)context {self = [super initWithTitle:title? ? ? ? ? ? ? ? ? ? message:message? ? ? ? ? ? ? ? ? delegate:self? ? ? ? ? cancelButtonTitle:@"No"? ? ? ? ? otherButtonTitles:@"Yes",nil];if (self) {? ? ? ? _ctxt = context;? ? ? ? _successHandler = [JSManagedValue managedValueWithValue:successHandler];// A JSManagedValue by itself is a weak reference. You convert it into a conditionally retained// reference, by inserting it to the JSVirtualMachine using addManagedReference:withOwner:? ? ? ? [context.virtualMachine addManagedReference:_successHandler withOwner:self];? ? ? ? _failureHandler = [JSManagedValue managedValueWithValue:failureHandler];? ? ? ? [context.virtualMachine addManagedReference:_failureHandler withOwner:self];? ? }returnself;}- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {if (buttonIndex ==self.cancelButtonIndex) {? ? ? ? JSValue *function = [self.failureHandler value];? ? ? ? [function callWithArguments:@[]];? ? }else {? ? ? ? JSValue *function = [self.successHandler value];? ? ? ? [function callWithArguments:@[]];? ? }? ? [self.ctxt.virtualMachine removeManagedReference:_failureHandler withOwner:self];? ? [self.ctxt.virtualMachine removeManagedReference:_successHandler withOwner:self];}@end
分析上面例子,從外部傳入的JSValue對象在類內部使用JSManagedValue來保存。
JSManagedValue本身是一個弱引用對象,需要調用JSVirtualMachine的addManagedReference:withOwner:把它添加到JSVirtualMachine對象中,確保使用過程中JSValue不會被釋放
當用戶點擊AlertView上的按鈕時,根據(jù)用戶點擊哪一個按鈕,來執(zhí)行對應的處理函數(shù),這時AlertView也隨即被銷毀。 這時需要手動調用removeManagedReference:withOwner:來移除JSManagedValue。