1. 程式人生 > >OC與JS互動之JavaScriptCore

OC與JS互動之JavaScriptCore

JavaScriptCore初探

在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
@property (strong, nonatomic) NSString *name;
@property (strong, nonatomic) NSString *description;
@end
@interface Item : NSObject
@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。

iOS載入webview時出現記憶體洩露導致閃退

解決這個問題的方法是在webViewDidFinishLoad方法中設定如下:

[[NSUserDefaults standardUserDefaults] setInteger:0 forKey:@"WebKitCacheModelPreferenceKey"];
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"WebKitDiskImageCacheEnabled"];//自己新增的,原文沒有提到。
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"WebKitOfflineWebApplicationCacheEnabled"];//自己新增的,原文沒有提到。
[[NSUserDefaults standardUserDefaults] synchronize];