關於UIWebView的總結
前言
今天參加了 Adobe 和 CSDN 組織的一個關於 PhoneGap 的開發講座 ,而 PhoneGap 在 iOS 裝置上的實現就是通過 UIWebView 控制元件來展示 html 內容,並且與 native 程式碼進行互動的。
正好我們在做有道雲筆記的 iPad 版,因為我們也是使用 UIWebView 來展示筆記內容,所以也需要做 js 與 native 程式碼相互呼叫的事情。所以在這兒順便總結一下 UIWebView 在使用上的細節,以及談談我對 PhoneGap 的看法。
機制
首先我們需要讓 UIWebView 載入本地 HTML。使用如下程式碼完成:
NSString * path = [[NSBundle mainBundle] bundlePath];
|
接著,我們需要讓 js 能夠呼叫 native 端。iOS SDK 並沒有原生提供 js 呼叫 native 程式碼的 API。但是 UIWebView 的一個 delegate 方法使我們可以做到讓 js 需要呼叫時,通知 native。在 native 執行完相應呼叫後,可以用 stringByEvaluatingJavaScriptFromString 方法,將執行結果返回給 js。這樣,就實現了 js 與 native 程式碼的相互呼叫。
以下是 PhoneGap 相關呼叫的示例程式碼:
// Objective-C 語言
|
具體讓 js 通知 native 的方法是讓 js 發起一次特殊的網路請求。這裡,我們和 PhoneGap 都是使用載入一個隱藏的 iframe 來實現的,通過將 iframe 的 src 指定為一個特殊的 URL,實現在 delegate 方法中截獲這次請求。
以下是 PhoneGap 相關呼叫的示例程式碼:
// Javascript 語言
// 通知 iPhone UIWebView 載入 url 對應的資源
// url 的格式為: gap:something
function loadURL(url) {
var iFrame;
iFrame = document.createElement("iframe");
iFrame.setAttribute("src", url);
iFrame.setAttribute("style", "display:none;");
iFrame.setAttribute("height", "0px");
iFrame.setAttribute("width", "0px");
iFrame.setAttribute("frameborder", "0");
document.body.appendChild(iFrame);
// 發起請求後這個 iFrame 就沒用了,所以把它從 dom 上移除掉
iFrame.parentNode.removeChild(iFrame);
iFrame = null;
}
|
在這裡,可能有些人說,通過改 document.location 也可以達到相同的效果。關於這個,我和 zyc 專門試過,一般情況下,改 document.location 是可以,但是改 document.location 有一個很嚴重的問題,就是如果我們連續 2 個 js 調 native,連續 2 次改 document.location 的話,在 native 的 delegate 方法中,只能截獲後面那次請求,前一次請求由於很快被替換掉,所以被忽略掉了。
我也專門去 Github 上查詢相關的開原始碼,它們都是用過 iframe 來實現呼叫的,例如這個:https://github.com/marcuswestin/WebViewJavascriptBridge
關於這個,我也做了一個 Demo 來簡單示例,地址如下:https://github.com/tangqiaoboy/UIWebViewSample
引數的傳遞
以上的示例程式碼為了講清楚機制,所以只是示例了最簡單的相互呼叫。但實際上 js 和 native 相互呼叫時,常常需要傳遞引數。
例如,有道雲筆記 iPad 版用 UIWebView 顯示筆記的內容,當使用者點選了筆記中的附件,這個時候,js 需要通知 native 到後臺下載這個筆記附件,同時通知 js 當前的下載進度。對於這個需求,js 層獲得使用者點選事件後,就需要把當前點選的附件的 ID 傳遞給 native,這樣 native 才能知道下載哪個附件。
引數傳遞最簡單的方式是將引數作為 url 的一部分,放到 iFrame 的 src 裡面。這樣 UIWebView 通過擷取分析 url 後面的內容即可獲得引數。但是這樣的問題是,該方法只能傳遞簡單的引數資訊,如果引數是一個很複雜的物件,那麼這個 url 的編解碼將會很複雜。對此,我們的有道雲筆記和 PhoneGap 採用了不同的技術方案。
- 我們的技術方案是將引數以 JSON 的形式傳遞,但是因為要附加在 url 之後,所以我們將 JSON 進行了 Base64 編碼,以保證 url 中不會出現一些非法的字元。
- PhoneGap 的技術方案是,也是用 JSON 傳遞引數,但是將 JSON 放在 UIWebView 中的一個全域性陣列中,UIWebView 當需要讀取引數時,通過讀取這個全域性陣列來獲得相應的引數。
相比之下,應該說 PhoneGap 的方案更加全面,適用於多種場景。而我們的方案簡潔高效,滿足了我們自己產品的需求。
同步和非同步
因為 iOS SDK 沒有天生支援 js 和 native 相互呼叫,大家的技術方案都是自己實現的一套呼叫機制,所以這裡面有同步非同步的問題。細心的同學就能發現,js 呼叫 native 是通過插入一個 iframe,這個 iframe 插入完了就完了,執行的結果需要 native 另外用 stringByEvaluatingJavaScriptFromString 方法通知 js,所以這是一個非同步的呼叫。
而 stringByEvaluatingJavaScriptFromString 方法本身會直接返回一個 NSString 型別的執行結果,所以這顯然是一個同步呼叫。
所以 js call native 是非同步,native call js 是同步。在處理一些邏輯的時候,不可避免需要考慮這個特點。
這裡順便說一個 android,其實在 android 開發中,js 調 native 是同步的,但是 PhoneGap 為了將自己做成一個跨平臺的框架,所以在 android 的 js call native 的 native 端,用 new Thread 新建了一個執行執行緒,這樣把 android 的 js call native 也變成了非同步呼叫。
UIWebView 的問題
執行緒阻塞問題
我們在開發中發現,當在 native 層呼叫 stringByEvaluatingJavaScriptFromString 方法時,可能由於 javascript 是單執行緒的原因,會阻塞原有 js 程式碼的執行。這裡我們的解決辦法是在 js 端用 defer 將 iframe 的插入延後執行。
主執行緒的問題
UIWebView 的 stringByEvaluatingJavaScriptFromString 方法必須是主執行緒中執行,而主執行緒的執行時間過長就會 block UI 的更新。所以我們應該儘量讓 stringByEvaluatingJavaScriptFromString 方法執行的時間短。
有道雲筆記在儲存的時候,需要呼叫 js 獲得筆記的完整 html 內容,這個時候如果筆記內容很複雜,就會執行很長一段時間,而因為這個操作必須是主執行緒執行,所以我們顯示 “正在儲存” 的 UIAlertView 完全無法正常顯示,整個 UI 介面完全卡住了。在新的編輯器裡,我們更新了獲得 html 內容的程式碼,才將這個問題解決。
鍵盤控制
做 iOS 開發的都知道,當我們需要鍵盤顯示在某個控制元件上時,可以呼叫 [obj becomeFirstResponder] 方法來讓鍵盤出來,並且游標輸入焦點出現在該控制元件上。
但是這個方法對於 UIWebView 並不可用。也就是說,我們無法通過程式控制讓游標輸入焦點出現在 UIWebView 上。
關於這個問題,我在 stackoverflow 上專門 問了一下,還是沒有得到很好的解決辦法。
CommonJS 規範
commonJS 是一個模組塊載入的規範。而 AMD 是該規範的一個草案,CommonJS AMD 規範描述了模組化的定義,依賴關係,引用關係以及載入機制,其規範原文在 這裡 。它被 requireJS,NodeJs,Dojo,jQuery 等開源框架廣泛使用。這裡 還有一篇不錯的中文介紹文章。
AMD 規範需要用目錄層級當作包層次,這一點就象 java 一樣。之前我以為 iOS 打包後的 ipa 資原始檔中不能有資源目錄層級關係,今天在會上問了一下,原來是我自己弄錯了。如果需要將目錄層級帶入 ipa 資原始檔中,只需要將該目錄拖入工程中,然後選擇 “Create groups for any added folders”。如下圖所示,這樣目錄層級能夠打包到 ipa 檔案中。
除錯
在 iOS 裝置中除錯 javascript 是一件相當苦逼的事情,拿 pw 的話來說:“一下子回到了 ie6 時代”。當然,業界也有一些除錯工具可以用的。
我們在開發時主要採用的是 weinre 這個框架。用這個框架,可以做一些基本的除錯工作,但是它現在功能還沒有象 pc 上的 js 偵錯程式那麼強大,例如它不能下斷點,另外如果有 js 執行錯誤,它也無法正確的將錯誤資訊報出。它還有一些 bug,例如在 mac 機下,如果你同時連線了有線網和無線網,那麼 weinre 將無法正確地連線到除錯頁面。
但終究,它是現在業界現存的唯一相對可用的除錯工具了。本次的 PhoneGap 講座的第一位演講者董龍飛有一篇部落格很好地介紹了 weinre 的使用,地址是 這裡,推薦感興趣的同學看看。即使不用 PhoneGap,weinre 也能給你在移動裝置上設計網頁帶來方便。
(2013 年 10 月 22 日更新):關於除錯這一塊兒,從 WWDC2012 開始,蘋果已經支援用 safari 來連線 iPhone 模擬器裡面的 UIWebView 進行除錯了,所以除錯上已經方便了很多。詳細的教程可以檢視: WWDC2012 Session 600《Debuging UIWebViews and Websites on iOS》
我對 PhoneGap 的看法
今天的大會上,2 位演講者把 PhoneGap 吹得相當牛。但是其實真正用過的人才能知道,PhoneGap 還是有相當多的問題的。至少我知道在網易就有一個使用 PhoneGap 而失敗的專案,所以我認為 PhoneGap 還是有它相當大的侷限性的。
我認為 PhoneGap 有以下 3 大問題:
-
首先,PhoneGap 的程式語言其實是 javascript,這對於非前端工作者來說,其實學習起來和學習原生的 objective-C 或 Java 程式語言難度差不多,而且由於歷史原因,javascript 語言本身的問題比其它語言都多。要想精通 javascript,相當不易。
-
然後,PhoneGap 的目標是方便地建立跨平臺的應用。但是其實蘋果和 google 都發布了自己的人機互動指南。有些情況下,蘋果的程式和 android 程式有著不同的互動原則的。象有道雲筆記的 iPhone 版 和 android 版,就有著完全不同的介面和互動。使用 PhoneGap 就意味著你的程式在 UI 和互動上,既不象原生的 iphone 程式,又不象原生的 android 程式。
-
最後,效能問題。Javascript 終究無法和原生的程式比執行效率,這一點在當你要做一些動畫效果的時候,就能顯現得很明顯。
當然,PhoneGap 的優勢也很明顯,如果你是做圖書類,查詢類,小工具類應用的話,這些應用 UI 互動不復雜,也不佔用很高的 cpu 資源,PhoneGap 將很好地發揮出它的優勢。對於這類應用:
-
你只需要編寫一次,則可以同時完成 iOS, android, windows phone 等版本的開發。
-
如果改動不大,只是內容升級,那它升級時只需要更新相應的 js 檔案,而不需要提交稽核,而一般正常提交蘋果的 app store 稽核的話,常常需要一週時間。
所以 PhoneGap 不是萬能的,但也不是沒有用,它有它擅長的領域,一切都看你是否合理地使用它。
最後,推薦 PhoneGap 中國網站 ,在這裡,你可以找到為數不多的中文資料。
對 js 的感想
現在前端工程師相當牛逼啊。前端工程師不但可以寫前端網頁,還可以用 Flex 寫桌面端程式,可以用 nodejs 寫 server 端程式,可以用 PhoneGap 寫移動端程式,這一切,都是基於 javascript 語言的,還有最新出的 windows 8,原生支援用 js 來寫 Metro 程式,世界已經無法阻止前端工程師了。