為何放棄第一種方案
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引擎來完成建議的雙向通信。