oc和js 交互(JavaScriptCore)

注: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。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,117評論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,860評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,128評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,291評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,025評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,421評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,477評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,642評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,177評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,970評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,157評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,717評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,410評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,821評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,053評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,896評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,157評論 2 375

推薦閱讀更多精彩內容