本文摘抄自:https://hjgitbook.gitbooks.io/ios/content/04-technical-research/04-javascriptcore-note.htmljavascript
注:JavaScriptCore API也能夠用Swift來調用,本文用Objective-C來介紹。html
在iOS7以前,原生應用和Web應用之間很難通訊。若是你想在iOS設備上渲染HTML或者運行JavaScript,你不得不使用UIWebView
。iOS7引入了JavaScriptCore
,功能更強大,使用更簡單。java
JavaScriptCore
是封裝了JavaScript和Objective-C橋接的Objective-C API,只要用不多的代碼,就能夠作到JavaScript調用Objective-C,或者Objective-C調用JavaScript。ios
在以前的iOS版本,你只能經過向UIWebView發送stringByEvaluatingJavaScriptFromString:
消息來執行一段JavaScript腳本。而且若是想用JavaScript調用Objective-C,必須打開一個自定義的URL(例如:foo://),而後在UIWebView的delegate方法webView:shouldStartLoadWithRequest:navigationType
中進行處理。git
JSValue: 表明一個JavaScript實體,一個JSValue能夠表示不少JavaScript原始類型例如boolean, integers, doubles,甚至包括對象和函數。
JSManagedValue: 本質上是一個JSValue,可是能夠處理內存管理中的一些特殊情形,它能幫助引用技術和垃圾回收這兩種內存管理機制之間進行正確的轉換。
JSContext: 表明JavaScript的運行環境,你須要用JSContext來執行JavaScript代碼。全部的JSValue都是捆綁在一個JSContext上的。
JSExport: 這是一個協議,能夠用這個協議來將原生對象導出給JavaScript,這樣原生對象的屬性或方法就成爲了JavaScript的屬性或方法,很是神奇。
JSVirtualMachine: 表明一個對象空間,擁有本身的堆結構和垃圾回收機制。大部分狀況下不須要和它直接交互,除非要處理一些特殊的多線程或者內存管理問題。github
JSVirtualMachine
爲JavaScript的運行提供了底層資源,JSContext
爲JavaScript提供運行環境,經過web
- (JSValue *)evaluateScript:(NSString *)script;
方法就能夠執行一段JavaScript腳本,而且若是其中有方法、變量等信息都會被存儲在其中以便在須要的時候使用。 而JSContext的建立都是基於JSVirtualMachine:objective-c
- (id)initWithVirtualMachine:(JSVirtualMachine *)virtualMachine;
若是是使用- (id)init;
進行初始化,那麼在其內部會自動建立一個新的JSVirtualMachine對象而後調用前邊的初始化方法。數組
建立一個 JSContext 後,能夠很容易地運行 JavaScript 代碼來建立變量,作計算,甚至定義方法:多線程
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值或對象。
例若有一個"Hello.js"文件內容以下:
function printHello() { }
在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,並執行JavaScript腳本,此時printHello函數並無被調用,只是被讀取到了這個context中。
而後從context中取出對printHello函數的引用,並保存到一個JSValue中。
注意這裏,從JSContext中取出一個JavaScript實體(值、函數、對象),和將一個實體保存到JSContext中,語法均與NSDictionary的取值存值相似,很是簡單。
最後若是JSValue是一個JavaScript函數,能夠用callWithArguments來調用,參數是一個數組,若是沒有參數則傳入空數組@[]。
仍是上面的例子,將"hello.js"的內容改成:
function printHello() { 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函數就能夠執行這個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是一個協議,可讓原生類的屬性或方法稱爲JavaScript的屬性或方法。
看下面的例子:
@protocol ItemExport <JSExport> @property (strong, nonatomic) NSString *name; @property (strong, nonatomic) NSString *description; @end @interface Item : NSObject <ItemExport> @property (strong, nonatomic) NSString *name; @property (strong, nonatomic) NSString *description; @end
注意Item類不去直接符合JSExport,而是符合一個本身的協議,這個協議去繼承JSExport協議。
例若有以下JavaScript代碼
function Item(name, description) { this.name = name; this.description = description; } var items = []; function addItem(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環境,等待稍後使用
[self.context setObject:Item.self forKeyedSubscript:@"Item"];
Objective-C的內存管理機制是引用計數,JavaScript的內存管理機制是垃圾回收。在大部分狀況下,JavaScriptCore能作到在這兩種內存管理機制之間無縫無錯轉換,但也有少數狀況須要特別注意。
Block會爲默認爲全部被它捕獲的對象建立一個強引用。JSContext爲它管理的全部JSValue也都擁有一個強引用。而且,JSValue會爲它保存的值和它所在的Context都維持一個強引用。這樣JSContext和JSValue看上去是循環引用的,然而並不會,垃圾回收機制會打破這個循環引用。
看下面的例子:
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,來防止循環引用。
當把一個JavaScript值保存到一個本地實例變量上時,須要尤爲注意內存管理陷阱。 用實例變量保存一個JSValue很是容易引發循環引用。
看如下下例子,自定義一個UIAlertView,當點擊按鈕時調用一個JavaScript函數:
#import <UIKit/UIKit.h> #import <JavaScriptCore/JavaScriptCore.h> @interface MyAlertView : UIAlertView - (id)initWithTitle:(NSString *)title message:(NSString *)message success:(JSValue *)successHandler failure:(JSValue *)failureHandler context:(JSContext *)context; @end
按照通常自定義AlertView的實現方法,MyAlertView須要持有successHandler,failureHandler這兩個JSValue對象
向JavaScript環境注入一個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環境中都是「強引用」(相對Objective-C的概念來講)的,這時JSContext強引用了一個presentNativeAlert函數,這個函數中又強引用了MyAlertView 等於說JSContext強引用了MyAlertView,而MyAlertView爲了持有兩個回調強引用了successHandler和failureHandler這兩個JSValue,這樣MyAlertView和JavaScript環境互相引用了。
因此蘋果提供了一個JSMagagedValue類來解決這個問題。
看MyAlertView.m的正確實現:
#import "MyAlertView.h" @interface XorkAlertView() <UIAlertViewDelegate> @property (strong, nonatomic) JSContext *ctxt; @property (strong, nonatomic) JSMagagedValue *successHandler; @property (strong, nonatomic) JSMagagedValue *failureHandler; @end @implementation MyAlertView - (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]; } return self; } - (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上的按鈕時,根據用戶點擊哪個按鈕,來執行對應的處理函數,這時AlertView也隨即被銷燬。 這時須要手動調用removeManagedReference:withOwner:
來移除JSManagedValue。