咱們都知道WebKit是個渲染引擎,簡單來講負責頁面的佈局,繪製以及層的合成,可是WebKit工程中不只僅有關於渲染相關的邏輯,也集成了默認的javascript引擎--JavaScriptCore,目前Safari的js引擎也基於JSC構建,不過有一些私有的優化,整體性能相差不大。JSC的執行理念比較符合傳統的引擎邏輯,它包括了2部分:解釋器和簡單方法JIT
。解釋器比較容易理解,針對某種類型的文件解釋執行,在JSC中,它的目標文件是由代碼構建的語法樹生成的字節碼文件,相似於java中的字節碼,不過在JSC中字節碼的執行是在基於寄存器的虛擬機中而不是基於棧,好處在於能夠方便的在ARM架構處理器中使用三地址指令,減小了次數較多的出棧和入棧等指令分派以及耗時的內存IO;JIT在java虛擬機中應用比較多,針對執行較屢次的熱點方法進行編譯爲本地方法,執行效率更高,JSC中的JIT同理。
在iOS7中,咱們能夠引入JSC框架,這樣,咱們能夠oc層來操做js層代碼的執行。另外JSC暴露了許多C層面的接口,咱們也能夠在底層來構建自定義的js執行環境,操做執行js代碼,可控執行可擴展性更強。javascript
既然有了這麼給力的引擎,咱們在構建hybrid app時可使用JSC來代替cordova的webViewJavascriptBridge框架完成簡易的接口暴露,將來在oc層逐漸能夠將UI組件模塊化,並經過JSExport暴露接口,由js層負責調用相應模塊的初始化方法完成界面的hybrid化。
oc端初始化一個js執行上下文JSContext對象很容易, [[JSContext alloc] init]
便可,可是在hybrid app中,經過這種方式初始化JSContext與承載頁面的UIWebVIew並非同一個js環境,所以咱們須要獲取UIWebView對應的JSContext。可是apple官方並未提供相關的方法,不過這邊難不倒某些人,有些人發現,經過KVC的方式可獲取UIWebView對應的JSContext,方式以下[webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]
。一旦獲取到對應的JSContext,咱們能夠作的就有不少了。java
// 獲取對應的JSContext JSContext *context=[webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]; // 設置JSContext的錯誤處理函數 [context setExceptionHandler:^(JSContext *context, JSValue *value) { NSLog(@"oc catches the exception: %@", value); }]; // 組件化某個功能類或UIController ShowjoyFad *sf=[ShowjoyFad new]; // 暴露改類至JSContext中,在js層的全局屬性中咱們能夠訪問該類,如window.showjoyFad context[@"showjoyFad"]=sf; context[@"ViewController"] = self; // 引用js層定義的函數 JSValue * abc = context[@"abc"]; // 執行 JSValue * ret = [abc callWithArguments:@[@"helloworld"]]; NSLog(@"ret: %@",[ret toString]);
經過簡單的例子能夠很明顯的看出JSC通訊的簡潔性,與android的WebView通訊相似,native端能夠直接講接口注入到js上下文中,js在什麼時候的時機調用函數。可是這裏涉及到一個比較棘手的問題,JSContext的獲取實在UIWebView的那個階段呢?
我作過一個測試:首先在UIWebView的webViewDidStartLoad階段建立JSContext並暴露oc端的方法,在加載一級頁面時js正常調用oc的方法,而跳轉到二級頁面中卻沒法執行oc的方法;而在webViewDidStartLoad階段因爲並未加載完js文件, 所以js層定義的函數在oc端沒法執行。
其次,在webVIewDidFinishLoad階段建立JSContext並透出oc方法,因爲加載js階段在webVIewDidFinishLoad階段以前,所以一級頁面js沒法調用oc方法,可是二級頁面同理也是如此,可是因爲js代碼是在iOS的UI線程執行,所以爲了讓js能夠調用oc方法,能夠經過在js設置setTimeout來讓任務放到執行隊列的末端,先執行oc層的webVIewDidFinishLoad方法,待任務完成後再執行js中的異步代碼,經過這種方式能夠完成js調用oc方法;反過來,oc層調用js函數沒有任何問題,由於在webVIewDidFinishLoad階段js代碼已執行完畢(除了異步代碼)。
爲此,能夠經過實現一個簡易的框架來完成js層和oc層的交互,爲了更好的兼容性,只有在webVIewDidFinishLoad階段建立JSContext。而在js層則有兩種方式來監測並執行oc的方法:android
1,在oc層的webVIewDidFinishLoad階段,暴露oc接口以後,經過JSContext或者UIWebView的stringByEvluateJavascriptString方法構建一個```webViewDidFinishLoad```事件,js端進行偵聽並調用 2,簡單的經過setTimeout將js的執行順序排至隊列末端
經過上述方法,構建了一個簡單的JSCBridge,可是缺點也很明顯,對oc端接口暴露時機有硬性要求,而且js執行oc端的代碼始終是異步,有違咱們的初衷。web
上篇中,咱們經過簡單的kvc獲取UIWebVIew的JSContext,可是實際上,apple並未給開發者提供訪問UIWebView的方法,雖然經過KVC可達到目標,可是當APP採用該種hack方法時,有很大概率不能經過APP Store的審覈,這對於一個基於上線的商業APP而言是難以忍受的,因此咱們必須尋找另外一種方法來獲取UIWebView的JSContext並且足夠安全易用,所以咱們需轉移目光。安全
在OS X中,WebFrameLoadDelegate負責WebKit與NSWebView的通訊,因爲NSWebView內部仍然使用WebKit渲染引擎,若要偵聽渲染過程當中的一系列事件,則必須使用WebFrameLoadDelegate對象:
一、加載過程:網絡
在一個訪問一個網頁的的整個過程,包括開始加載,加載標題,加載結束等。webkit都會發送相應的消息給WebFrameLoadDelegate 。 webView:didStartProvisionalLoadForFrame:開始加載,在這裏獲取加載的url webView:didReceiveTitle:forFrame:獲取到網頁標題 webView:didFinishLoadForFrame:頁面加載完成
二、錯誤的處理:架構
加載的過程中,有可能會發生錯誤。錯誤的消息也會發送給WebFrameLoadDelegate 。咱們能夠在這兩個函數裏面對錯誤信息進行處理 webView:didFailProvisionalLoadWithError:forFrame: 這個錯誤發生在請求數據以前,最多見是發生在無效的URL或者網絡斷開沒法發送請求 webView:didFailLoadWithError:forFrame: 這個錯誤發生在請求數據以後
但是在iOS中呢?我嘗試過,並無WebFrameLoadDelegate這個對象,看來iOS中的WebKit框架並未提供UIWebView這麼多的接口,可是有些人經過WebKit的源碼仍是發現了一二,他就是Nick Hodapp。app
在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引擎來完成建議的雙向通訊。