原:https://github.com/YanlongMa/SwiftJavaScriptCore
注: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中進行處理猜拾。
然而現在可以利用JavaScriptCore的先進功能了即舌,它可以:
運行JavaScript腳本而不需要依賴UIWebView
使用現代Objective-C的語法(例如Blocks和下標)
在Objective-C和JavaScript之間無縫的傳遞值或者對象
創(chuàng)建混合對象(原生對象可以將JavaScript值或函數作為一個屬性)
使用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實現,iOS端和Android端都可以調用
JavaScriptCore概述
JSValue: 代表一個JavaScript實體磨总,一個JSValue可以表示很多JavaScript原始類型例如boolean, integers, doubles,甚至包括對象和函數笼沥。
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 值:字符串和數字遇绞;數組、對象和方法燎窘;甚至錯誤和特殊的 JavaScript 值諸如 null 和 undefined摹闽。
可以對JSValue調用toString、toBool褐健、toDouble付鹿、toArray等等方法把它轉換成合適的Objective-C值或對象。
Objective-C調用JavaScript
例如有一個"Hello.js"文件內容如下:
functionprintHello(){
}
在Objective-C中調用printHello方法:
NSString*scriptPath = [[NSBundlemainBundle] pathForResource:@"hello"ofType:@"js"];
NSString*scriptString = [NSStringstringWithContentsOfFile:scriptPath encoding:NSUTF8StringEncodingerror:nil];
JSContext *context = [[JSContext alloc] init];
[context evaluateScript:scriptString];
JSValue *function =self.context[@"printHello"];
[function callWithArguments:@[]];
分析以上代碼:
首先初始化了一個JSContext蚜迅,并執(zhí)行JavaScript腳本舵匾,此時printHello函數并沒有被調用,只是被讀取到了這個context中谁不。
然后從context中取出對printHello函數的引用坐梯,并保存到一個JSValue中。
注意這里刹帕,從JSContext中取出一個JavaScript實體(值吵血、函數、對象)偷溺,和將一個實體保存到JSContext中蹋辅,語法均與NSDictionary的取值存值類似,非常簡單挫掏。
最后如果JSValue是一個JavaScript函數侦另,可以用callWithArguments來調用,參數是一個數組尉共,如果沒有參數則傳入空數組@[]褒傅。
JavaScript調用Objective-C
還是上面的例子,將"hello.js"的內容改為:
functionprintHello(){
print("Hello, World!");
}
這里的print函數用Objective-C代碼來實現
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函數就可以執(zhí)行這個Objective-C Block樊卓。
注意這里JavaScript中的字符串可以無縫的橋接為NSString,實參"Hello, World!"被傳遞給了NSString類型的text形參杠河。
異常處理
當JavaScript運行時出現異常碌尔,會回調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;
}
varitems = [];
functionaddItem(item){
items.push(item);
}
可以在Objective-C中把Item對象傳遞給addItem函數
Item *item = [[Item alloc] init];
item.name=@"itemName";
item.description=@"itemDescription";
JSValue *function = context[@"addItem"];
[function callWithArguments:@[item]];
或者把Item類導出到JavaScript環(huán)境叹坦,等待稍后使用
[self.contextsetObject:Item.selfforKeyedSubscript:@"Item"];
內存管理陷阱
Objective-C的內存管理機制是引用計數,JavaScript的內存管理機制是垃圾回收卑雁。在大部分情況下募书,JavaScriptCore能做到在這兩種內存管理機制之間無縫無錯轉換绪囱,但也有少數情況需要特別注意。
在block內捕獲JSContext
Block會為默認為所有被它捕獲的對象創(chuàng)建一個強引用莹捡。JSContext為它管理的所有JSValue也都擁有一個強引用鬼吵。并且,JSValue會為它保存的值和它所在的Context都維持一個強引用篮赢。這樣JSContext和JSValue看上去是循環(huán)引用的齿椅,然而并不會,垃圾回收機制會打破這個循環(huán)引用启泣。
看下面的例子:
self.context[@"getVersion"] = ^{
NSString*versionString = [[NSBundlemainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
versionString = [@"version "stringByAppendingString:versionString];
JSContext *context = [JSContext currentContext];// 這里不要用self.context
JSValue *version = [JSValue valueWithObject:versionString inContext:context];
returnversion;
};
使用[JSContext currentContext]而不是self.context來在block中使用JSContext涣脚,來防止循環(huán)引用。
JSManagedValue
當把一個JavaScript值保存到一個本地實例變量上時寥茫,需要尤其注意內存管理陷阱遣蚀。 用實例變量保存一個JSValue非常容易引起循環(huán)引用。
看以下下例子纱耻,自定義一個UIAlertView妙同,當點擊按鈕時調用一個JavaScript函數:
#import
#import
@interfaceMyAlertView:UIAlertView
- (id)initWithTitle:(NSString*)title
message:(NSString*)message
success:(JSValue *)successHandler
failure:(JSValue *)failureHandler
context:(JSContext *)context;
@end
按照一般自定義AlertView的實現方法,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函數胰耗,這個函數中又強引用了MyAlertView 等于說JSContext強引用了MyAlertView限次,而MyAlertView為了持有兩個回調強引用了successHandler和failureHandler這兩個JSValue,這樣MyAlertView和JavaScript環(huán)境互相引用了柴灯。
所以蘋果提供了一個JSMagagedValue類來解決這個問題卖漫。
看MyAlertView.m的正確實現:
#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= [superinitWithTitle: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.virtualMachineaddManagedReference:_successHandler withOwner:self];
_failureHandler = [JSManagedValue managedValueWithValue:failureHandler];
[context.virtualMachineaddManagedReference:_failureHandler withOwner:self];
}
returnself;
}
- (void)alertView:(UIAlertView*)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
if(buttonIndex ==self.cancelButtonIndex) {
JSValue *function = [self.failureHandlervalue];
[function callWithArguments:@[]];
}else{
JSValue *function = [self.successHandlervalue];
[function callWithArguments:@[]];
}
[self.ctxt.virtualMachineremoveManagedReference:_failureHandler withOwner:self];
[self.ctxt.virtualMachineremoveManagedReference:_successHandler withOwner:self];
}
@end
分析上面例子,從外部傳入的JSValue對象在類內部使用JSManagedValue來保存赠群。
JSManagedValue本身是一個弱引用對象羊始,需要調用JSVirtualMachine的addManagedReference:withOwner:把它添加到JSVirtualMachine對象中,確保使用過程中JSValue不會被釋放
當用戶點擊AlertView上的按鈕時查描,根據用戶點擊哪一個按鈕突委,來執(zhí)行對應的處理函數,這時AlertView也隨即被銷毀冬三。 這時需要手動調用removeManagedReference:withOwner:來移除JSManagedValue匀油。
參考資料
《iOS 7 by tutorials》
https://www.bignerdranch.com/blog/javascriptcore-and-ios-7/
http://nshipster.com/javascriptcore/