1. 程式人生 > 其它 >iOS引入JavaScriptCore引擎框架(二)

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引擎來完成建議的雙向通訊。