初識JavaScriptCore
JavaScriptCore介紹
-
OS X Mavericks 和 iOS 7 引入了 JavaScriptCore 庫,它把 WebKit 的 JavaScript 引擎用 Objective-C 封裝,提供了簡單,快速以及安全的方式接入世界上最流行的語言。不管你愛它還是恨它,JavaScript 的普遍存在使得程序員、工具以及融合到 OS X 和 iOS 里這樣超快的虛擬機中資源的使用都大幅增長.
-
在之前的版本,你只能通過向UIWebView發送stringByEvaluatingJavaScriptFromString:消息來執行一段JavaScript腳本,并且如果想用JavaScript調用OC,必須打開一個自定義的URL(例如axe://),然后在webView:shouldStartLoadWithRequest:navigationType:中處理.
-
JavaScriptCore的先進功能
-
運行JavaScript腳本而不需要依賴UIWebView
-
使用現代Objective-C的語法(例如Blocks和下標)
-
在Objective-C和JavaScript之間無縫的傳遞值或者對象
-
創建混合對象(原生對象可以將JavaScript值或函數作為一個屬性)
JavaScriptCore概述
-
JSValue :代表一個JavaScript實體,一個JSValue可以表示很多JavaScript原始類型例如boolean, integers, doubles,甚至包括對象和函數.
-
JSManagedValue :本質上是一個JSValue,但是可以處理內存管理中的一些特殊情形,它能幫助引用計數和垃圾回收這兩種內存管理機制之間進行正常的切換.
-
JSContext :代表JavaScript的運行環境,你需要用JSContext來執行JavaScript代碼,所有的JSValue都是捆綁在一個JSContext上的.
-
JSExport :這是一個協議,可以用這個協議將原生對象導出給JavaScript,這樣原生對象的屬性或者方法就成了JavaScript的屬性或者方法,很神奇!
-
JSVirtualMachine :代表一個對象空間,擁有自己的堆結構和垃圾回收機制,大部分情況下不需要和它直接交互,除非要處理一些特殊的多線程或者內存管理問題.
JSContext/JSValue
-
JSContext是運行JavaScript代碼的環境,一個JSContext是一個全局環境的實例,如果你寫過一個在瀏覽器內運行的JavaScript, JSContext類似于window,創建一個JSContext后,可以很容易的運行JavaScript代碼來創建變量,做計算, 甚至定義方法:
JSContext *context = [[JSContext alloc] init]; [context evaluateScript:@"var num = 1 + 2"]; [context evaluateScript:@"var names = ['Giant', 'Axe', 'GA']"]; [context evaluateScript:@"var triple = function(value) { return value * 3 }"]; JSValue *tripleNum = [context evaluateScript:@"triple(num)"];
-
代碼的最后一行,任何出自JSContext的值都被包裹在一個JSValue對象中,像JavaScript這樣的動態語言需要一個動態類型,所以JSValue包裝了每一個可能的JavaScript值:字符串;數字;數組;對象;方法;甚至錯誤和特殊的JavaScript值比如null和undefined.
-
JSValue包括了一系列方法用于訪問其可能的值以保證有正確的Foundation類型,包括如下:
JavaScript Type JSValue Method Objective-C Type Swift Type string toString NSString String! boolean toBool BOOL Bool number toNumber,toDouble,toInt32,toUInt32 NSNumber,double,int32_t,uint32_t NSNumber!,Double,Int32,UInt32 Date toDate NSDate NSDate! Array toArray NSArray [AnyObject]! Object toDictionary NSDictionary [NSObject : AnyObject]! Object toObject,toObjectOfClass: custom type custom type -
從上面的例子中得到tripleNum的值,只需使用適當的方法:
NSLog(@"Tripled: %d", [tripleNum toInt32]); // Tripled: 9
下標值
-
對JSContext和JSValue實例使用下標的方式,我們可以很容易的訪問我們之前創建的context的任何值. JSContext需要一個字符串下標,而JSValue允許使用字符串或整數標來得到里面的對象和數組:
JSValue *names = context[@"names"]; JSValue *initialName = names[0]; NSLog(@"The first name: %@", [initialName toString]); // The first name: Giant
調用方法
-
JSValue包裝了一個JavaScript函數,我們可以從OC代碼中使用Foundation類型作為參數直接調用該函數:
JSValue *tripleFunction = context[@"triple"]; JSValue *result = [tripleFunction callWithArguments:@[@5]]; NSLog(@"Five tripled: %d", [result toInt32]);
錯誤處理
-
JSContext還有另外一個有用的招數,通過設置上下文的exceptionHandler屬性,你可以觀察和記錄語法,類型以及運行時錯誤,exceptionHandler是一個接收一個JSContext引用和異常本身的回調處理:
context.exceptionHandler = ^(JSContext *context, JSValue *exception) { NSLog(@"JS Error: %@", exception); }; [context evaluateScript:@"function multiply(value1, value2) { return value1 * value2 "]; // JS Error: SyntaxError: Unexpected end of script
JavaScript調用
oc調js
-
例如有一個"Hello.js"文件內容如下:
function printHello() { }
-
在Objective-C中調用printHello方法:
// 取出js路徑 NSString *scriptPath = [[NSBundle mainBundle] pathForResource:@"hello" ofType:@"js"];
// UTF8編碼 NSString *scriptString = [NSString stringWithContentsOfFile:scriptPath encoding:NSUTF8StringEncoding error:nil]; // 初始化JSContext JSContext *context = [[JSContext alloc] init]; // 執行JavaScript腳本 [context evaluateScript:scriptString]; // 取出printHello函數,保存到JSValue中 JSValue *function = self.context[@"printHello"]; // 調用(如果JSValue是一個js函數,可以用callWithArguments來調用,參數是一個數組,如果沒有參數則傳入空數組@[]) [function callWithArguments:@[]];</code></pre>
js調oc
JS調用OC有兩個方法:block和JSExport protocol。
-
Block 方法:
// 初始化JSContext self.context = [[JSContext alloc] init];
// 定義block保存到context中 self.context[@"add"] = ^(NSInteger a, NSInteger b) { NSLog(@"addNum : %@", @(a + b)); }; // 執行javaScript [self.context evaluateScript:@"add(2,3)"];</code></pre>
- JSExport 方法:
- 新建一個類,遵守一個我們自定義的繼承自JSExport的協議:
- 然后我們在VC里測試.
#import <Foundation/Foundation.h>
import <JavaScriptCore/JavaScriptCore.h>
@protocol JSTestDelegate <JSExport>
// 測試無參數
- (void)testNoPara; // 測試一個參數
- (void)testOnePara:(NSString *)msg; // 測試兩個參數
- (void)testTwoPara:(NSString )msg1 secondPara:(NSString )msg2;
@end
@interface testJSObject : NSObject <JSTestDelegate>
@end</code></pre>
#import "testJSObject.h"
@implementation testJSObject
(void)testNoPara { NSLog(@"no para"); }
(void)testOnePara:(NSString *)msg { NSLog(@"one para"); }
(void)testTwoPara:(NSString )msg1 secondPara:(NSString )msg2 { NSLog(@"two para"); }
@end</code></pre>
// 創建JSContext self.context = [[JSContext alloc] init];
//設置異常處理 self.context.exceptionHandler = ^(JSContext *context,JSValue *exception) { [JSContext currentContext].exception = exception; NSLog(@"exception:%@",exception); }; // 將testObj添加到context中 testJSObject *testObj = [testJSObject new]; self.context[@"testObject"] = testObj; NSString *jsStr1 = @"testObject.testNoPara()"; NSString *jsStr2 = @"testObject.testOnePara()"; [self.context evaluateScript:jsStr1]; [self.context evaluateScript:jsStr2];</code></pre>
- demo比較簡單,控制臺輸出結果如下:
打印結果
-
唯一要注意的是OC的函數命名和JS函數命名規則問題,協議中定義的 testNoPara , testOnePara: , testTwoPara:secondPara: js調用時要注意.
內存管理陷阱
-
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; };</code></pre>
-
使用[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</code></pre>
-
按照一般自定義AlertView的實現方法,MyAlertView需要持有 successHandler , failureHandle 這兩個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環境互相引用了。
-
所以蘋果提供了一個JSManagedValue類來解決這個問題。
-
看MyAlertView.m的正確實現:
#import "MyAlertView.h"
@interface MyAlertView() <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</code></pre>
-
分析上面例子,從外部傳入的JSValue對象在類內部使用JSManagedValue來保存.
-
JSManagedValue本身是一個弱引用對象,需要調用JSVirtualMachine的addManagedReference:withOwner:
把它添加到JSVirtualMachine對象中,確保使用過程中JSValue不會被釋放. -
當用戶點擊AlertView上的按鈕時,根據用戶點擊哪一個按鈕,來執行對應的處理函數,這時AlertView也隨即被銷毀.這時需要手動調用removeManagedReference:withOwner:
來移除JSManagedValue.
參考資料
- 《iOS 7 by tutorials》
- https://developer.apple.com/videos/play/wwdc2013-615/
- http://nshipster.com/javascriptcore/
來自:http://www.jianshu.com/p/41b36929665a
-