Objc與JS間相互調用

from: http://my.oschina.net/fwj/blog/471035?p={{page}}

過去3、4年都在進行跨平臺的混合應用開發,但一直沒有系統梳理跨平臺技術的底層原理,趁新工作未正式入職,這裏整理一下。跨平臺的一種實現是基於webview。所謂webview,實質是在原生app中打開一個內嵌瀏覽器,具體到iOS平臺就是使用UIWebView這個控件。然後就很容易理解了,我們相當於開發一個webapp(網頁應用),然後通過原生應用作爲用戶入口(而非原生瀏覽器),用戶會訪問到遠程服務器的網頁內容。從用戶感知上,似乎是在使用一個App,但實際上是在訪問一個網頁。

以上,只是跨平臺基於webview實現的工作原理,而更重要的是如何橋接webview的js和app的objc,使得webapp也可用使用原生的功能api,如調用攝像頭等,而app又可以調用webview裏的js,即向雙通信。

Apple開放了一個叫做JavascriptCore的框架,此框架最早在OSX10.2就存在,但到了2013年在OSX10.9上才發佈其調用的API,而後又在iOS7上公開,由此我們可用名正言順地使用了。

JavascriptCore提供了以下幾個API,實現跨平臺通信:

  • JavascriptCore/API/JSContext.h
  • JavascriptCore/API/JSExport.h 
  • JavascriptCore/API/JSValue.h

JSContext是JavascriptCore的主入口,它代表了JS的運行時環境,在其中可以定義對象、方法等,這些實體(對象、方法)的生命週期在JSContext被釋放的時候才結束。而且可用指定的JSVirtualMachine來創建JSContext,每個JSVM都會獨立運行在一個線程上。

我們可用通過JSContext的evaluateScript方法來定義我們的JS方法,而且是通過字符串定義代碼,當然可以通過讀取外邊js文件來實現。看下面的例子:

        // getting a JSContext
        JSContext *context = [JSContext new];

        // defining a JavaScript function
        NSString *jsFunctionText =
        @"var isValidNumber = function(phone) {"
        "    var phonePattern = /^[0-9]{3}[ ][0-9]{3}[-][0-9]{4}$/;"
        "    return phone.match(phonePattern) ? true : false;"
        "}";
        [context evaluateScript:jsFunctionText];

這裏,相當於在objc層,向JSContext注入了一個isValidNumber的js方法。

正如上文所述,JSContext代表了一個JS運行環境,而我們的示例代碼都是單獨創建這個JSContext運行環境的,實際上UIWebView實例也有它自己的JSContext運行環境。爲了修改web上的內容,我們需要訪問UIWebView的JSContext。

但Apple就是一個悶騷男,雖然已經公開了JavascriptCore的API,但又不提供直接訪問UIWebView’s JSContext的方法。

幸好“key-value”把我們救了回來:

// get JSContext from UIWebView instance
JSContext *context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

然後,我們可以通過JSValue來獲取JSContext中js方法的引用和執行的結果:

        // 獲取isValidNumber方法的引用
        JSValue *jsFunction = context[@"isValidNumber"];
        // 通過callWithArguments方法調用js方法
        JSValue *value = [jsFunction callWithArguments:@[ phone ]];

JavascriptCore會自動轉換JSValue的對象類型,比如這裏isValidNumber返回的boolean,同時還支持NSString, NSDate, NSDictionary, NSArray等。

另外,我們還可以增加異常捕捉

[context setExceptionHandler:^(JSContext *context, JSValue *value) {
        NSLog(@"%@", value);
}];

再有,通過JSExport可以將objc的方法暴露給JS。

  @protocol BNRContactAppJS <JSExport>

    - (void)addContact:(BNRContact *)contact;

    @end

    @interface BNRContactApp : NSObject <BNRContactAppJS>
    ...
    @end

addContact這個方法是在BNRContactAppJS協議中聲明的,BNRContactAppJS又源自於JSExport,所以addContact方法將會暴露給JS環境,而其他方法則對JS環境而言是隱藏的。

最後,我們看一個完整的例子

- (void)webViewDidFinishLoad:(UIWebView *)webView
    {
        // get JSContext from UIWebView instance
        JSContext *context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

        // enable error logging
        [context setExceptionHandler:^(JSContext *context, JSValue *value) {
            NSLog(@"WEB JS: %@", value);
        }];

        // give JS a handle to our BNRContactApp instance
        context[@"myApp"] = self.app;

        // register BNRContact class
        context[@"BNRContact"] = [BNRContact class];

        // add function for processing form submission
        NSString *addContactText =
        @"var contactForm = document.forms[0];"
         "var addContact = function() {"
         "    var name = contactForm.name.value;"
         "    var phone = contactForm.phone.value;"
         "    var address = contactForm.address.value;"
         "    var contact = BNRContact.contactWithNamePhoneAddress(name, phone, address);"
         "    myApp.addContact(contact);"
         "};"
         "contactForm.addEventListener('submit', addContact);";
        [context evaluateScript:addContactText];
    }

最終跨平臺調用就在這一句:

myApp.addContact(contact);

在JS環境調用了objc的方法。

總結一下:

  • ==從objc調用js:JSContext的evaluateScriptf方法和JSValue的callWithArguments方法;==
  • ==捕捉JS執行的異常;==
  • ==從WebView實例獲取JSContext;==
  • ==通過JSExport將objc方法暴露給js調用。==

最後囉嗦一下,iOS7以前,並沒有JavascriptCore,所以多使用 stringByEvaluatingJavaScriptFromString。

Titanium 就是使用了JavascriptCore的方式。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章