本文摘抄自:https://hjgitbook.gitbooks.io/ios/content/04-technical-research/04-javascriptcore-note.html
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發送stringByEvaluatingJavaScriptFromString:
消息來執行一段JavaScript腳本。並且如果想用JavaScript調用Objective-C,必須打開一個自定義的URL(例如:foo://),然后在UIWebView的delegate方法webView:shouldStartLoadWithRequest:navigationType
中進行處理。
然而現在可以利用JavaScriptCore的先進功能了,它可以:
- 運行JavaScript腳本而不需要依賴UIWebView
- 使用現代Objective-C的語法(例如Blocks和下標)
- 在Objective-C和JavaScript之間無縫的傳遞值或者對象
- 創建混合對象(原生對象可以將JavaScript值或函數作為一個屬性)
使用Objective-C和JavaScript結合開發的好處:
- 快速的開發和制作原型:
如果某塊區域的業務需求變化的非常頻繁,那么可以用JavaScript來開發和制作原型,這比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的運行環境,你需要用JSContext來執行JavaScript代碼。所有的JSValue都是捆綁在一個JSContext上的。
JSExport: 這是一個協議,可以用這個協議來將原生對象導出給JavaScript,這樣原生對象的屬性或方法就成為了JavaScript的屬性或方法,非常神奇。
JSVirtualMachine: 代表一個對象空間,擁有自己的堆結構和垃圾回收機制。大部分情況下不需要和它直接交互,除非要處理一些特殊的多線程或者內存管理問題。
JSContext / JSValue
JSVirtualMachine
為JavaScript的運行提供了底層資源,JSContext
為JavaScript提供運行環境,通過
- (JSValue *)evaluateScript:(NSString *)script;
方法就可以執行一段JavaScript腳本,並且如果其中有方法、變量等信息都會被存儲在其中以便在需要的時候使用。 而JSContext的創建都是基於JSVirtualMachine:
- (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值或對象。
Objective-C調用JavaScript
例如有一個"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來調用,參數是一個數組,如果沒有參數則傳入空數組@[]。
JavaScript調用Objective-C
還是上面的例子,將"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
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
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,來防止循環引用。
JSManagedValue
當把一個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。