天天看點

iOS引入JavaScriptCore引擎架構(二)

為何放棄第一種方案

UIWebView的JSContext擷取

    上篇中,我們通過簡單的kvc擷取UIWebVIew的JSContext,但是實際上,apple并未給開發者提供通路UIWebView的方法,雖然通過KVC可達到目标,但是當APP采用該種hack方法時,有很大幾率不能通過APP Store的稽核,這對于一個基于上線的商業APP而言是難以忍受的,是以我們必須尋找另一種方法來擷取UIWebView的JSContext而且足夠安全易用,是以我們需轉移目光。

解決

WebFrameLoadDelegate

    在OS X中,WebFrameLoadDelegate負責WebKit與NSWebView的通信,由于NSWebView内部仍然使用WebKit渲染引擎,若要偵聽渲染過程中的一系列事件,則必須使用WebFrameLoadDelegate對象:

        1、加載過程:

在一個通路一個網頁的的整個過程,包括開始加載,加載标題,加載結束等。webkit都會發送相應的消息給WebFrameLoadDelegate 。

webView:didStartProvisionalLoadForFrame:開始加載,在這裡擷取加載的url
              webView:didReceiveTitle:forFrame:擷取到網頁标題
              webView:didFinishLoadForFrame:頁面加載完成
           

        2、錯誤的處理:

加載的過程當中,有可能會發生錯誤。錯誤的消息也會發送給WebFrameLoadDelegate 。我們可以在這兩個函數裡面對錯誤資訊進行處理

webView:didFailProvisionalLoadWithError:forFrame: 這個錯誤發生在請求資料之前,最常見是發生在無效的URL或者網絡斷開無法發送請求
             webView:didFailLoadWithError:forFrame: 這個錯誤發生在請求資料之後
           

    可是在iOS中呢?我嘗試過,并沒有WebFrameLoadDelegate這個對象,看來iOS中的WebKit架構并未提供UIWebView這麼多的接口,但是有些人通過WebKit的源碼還是發現了一二,他就是Nick Hodapp。

Nick的發現

    在iOS中,盡管沒有暴露WebFrameLoadDelegate,但是在具體實作上仍會判斷WebKit的implement有沒有實作這個協定的某些方法,如果實作則仍會執行,而且在webit的WebFrameLoaderClient.mm檔案中,

if (implementations->didCreateJavaScriptContextForFrameFunc) {
    CallFrameLoadDelegate(implementations->didCreateJavaScriptContextForFrameFunc, webView, @selector(webView:didCreateJavaScriptContext:forFrame:),
        script.javaScriptContext(), m_webFrame.get());
}
           

會判斷目前的對象有沒有實作

webView:didCreateJavaScriptContext:forFrame:

方法,有則執行。該方法會傳遞三個參數,第一個是與webkit通信的WebView(此WebView并不是UIWebVIew,Nick層做過測試通過擷取的WebView并不能周遊到我們需要的UIWebVIew,是以推測,這個WebView是一個UIView的proxy對象,不是UIView類);第二個則是我們想要擷取的JSContext;第三個參數是webkit架構中的WebFrame對象,與我們的期望無關。

    為了讓webkit執行這個函數,我們必須讓對象實作這個方法。由于所有的OC對象都繼承自NSObject對象,是以我們可以在NSObject對象上實作該方法,這樣可以保證該段代碼可以在webkit架構中執行。

    其次,我們既然擷取到了JSContext,但是并不知道JSContext與UIWebVIew的對應關系,我們的ViewController中可能會有多個UIWebView,如何将擷取的JSContext與UIWebview對應起來也是一個難題。在此處有一個簡單的方法,就是擷取所有的UIWebView對象,在每個對象中執行一段js代碼,在js上下文設定一個變量做為标記,然後在我們擷取的JSContext中判斷該變量是否與周遊的UIWebVIew對象中的對象是否相等來擷取。這樣,我們可以在UIWebView的webViewDidStartLoad和webViewDidFinishLoad之間擷取到JSContext,進行oc和js的雙向通信。

完善

    我們通過上節的闡述,大緻明白了Nick的思路,是以可以通過協定和類别來完成這種通信機制,當然采用oc運作時也是可以的。最終oc端接口的代碼放在

webView:didCreateJavaScriptContext:forFrame:

中,這樣js檔案隻需加載完畢就可執行oc的接口方法;而oc端要通路js的接口則可在webVIewDidFinishLoad中執行,完美解決

接口通路時機

的問題。

    在js端,由于隻有暴露在全局的

函數聲明

才能夠讓oc端通路,這就限制了js端的靈活性。我嘗試過在js端通過“指派”完成接口的暴露(window.say = function(){alert("hello world!")};),在oc端無法通路,隻有通過普通的函數聲明才能解決問題,這可能與JSContext的記憶體指針引用相關,為了解決此問題,我通過建立一個全局函數來暴露js端的接口對象,通過擷取的對象來通路具體的接口方法。

if(isiOS4JSC){
    // 将注冊的方法透出到window.jscObj的屬性上
    var ev = eval;
    $.JSBridge._JSMethod = method;

    // 暴露函數至全局
    // jsc隻能執行全局函數聲明方式定義的函數,不可以将函數指針複制給其他變量執行
    ev('function toObjectCExec() {' +
      'window.jscObj = window.jscObj ? window.jscObj : {};'+
      'window.jscObj["' + methodName + '"] = function (message) {' +
      '  var ret = $.JSBridge._JSMethod(message);' +
      '  return JSON.stringify(ret);' +
      '};' +
      'return jscObj;' +
    '}');

  }
           

如此,js端的接口暴露就很容易了。

尾聲

    我現在仍然相信,目前的iOS hybridAPP的主流通信方式仍然适corava的javascriptWebViewBridge,但是随着jsc引入到iOS7中,本文介紹的使用jsc(嵌入js引擎的方式)來完成oc和js的通信将更為流行,盡管目前apple提供的針對jsc的開發接口文檔幾乎沒有,但是我們通過webkit的源碼做一些hack的方式也不是不可以,畢竟隻要UIWebView仍然使用webkit進行渲染,這種方式會一直有效,除非apple在代碼層面針對hack做過濾,不過這種可能性真的很小。我們有理由憧憬未來在iOS和android下更友善的內建js引擎來完成建議的雙向通信。