58 同城 iOS 客戶端 Hybrid 框架探索

軟件 編程語言 通信 WebKit IT優就業 2017-08-02

引言

Hybrid App 是指同時使用 Native 與 Web 的 App。Native 界面具有良好的用戶體驗,但是不易動態改變,且開發成本較高。對於變動較大的頁面,使用 Web 來實現是一個比較好的選擇,所以,目前很多主流 App 都採用 Native 與 Web 混合的方式搭建。58 同城客戶端上線不久即採用了 Hybrid 方式,至今已有六七年。而 iOS 客戶端的 Hybrid 框架在最初設計和演進的過程中,隨著時間推移和業務需求的不斷增加,遇到了許多問題。為了解決它們,整個 Hybrid 框架產生了很大的變化。本文將遇到的典型問題進行了總結,並重點介紹 58 iOS 採用的解決方案,希望能給讀者搭建自己的 Hybrid 框架一些參考。主要包括以下四個方面:

1. 通訊方式以及通訊框架

58 App 最初採用的 Web 調用 Native 的通訊方式是 AJAX 請求,不僅存在內存洩露問題,且 Native 在回調給 Web 結果時無法確定回調給哪個 Web View。另外,如何搭建一個簡單、實用、擴展性好的 Hybrid 框架是一個重點內容。這些內容將在通訊部分詳細介紹。

2. 緩存原理及緩存框架

提升 Web 頁面響應速度的一個有效手段就是使用緩存。58 iOS 客戶端如何對 Web 資源進行緩存以及如何搭建 Hybrid 緩存框架將在緩存部分介紹。

3. 性能

iOS 8 推出了 WebKit 框架,核心是 WKWebView,其在性能上要遠優於 UIWebView,並且提供了一些新的功能,但遺憾的是 WKWebView 不支持自定義緩存。我們經過調研和測試發現了一些從 UIWebView 升級到 WKWebView 的可行解決方案,將在性能部分重點介紹。

4. 耦合

58 iOS 客戶端最初的 Hybrid 框架設計過於簡單,導致 Web 載體頁漸漸變得十分臃腫,繼承關係十分複雜。耦合部分詳細介紹了平穩解決載體頁耦合問題的方案。

通訊

Hybrid 框架首先要考慮的問題就是 Web 與 Native 之間的通訊。蘋果在 iOS 7 系統推出了 JavaScriptCore.framework 框架,通過該框架可以方便地實現 JavaScript 與 Native 的通訊工作。但是在 58 App 最早引入 Hybrid 時,需要支持 iOS 7 以下的系統版本,所以 58 App 並沒有使用 JavaScriptCore.framework,而是採用了更原始的方式。

傳統的通訊方式(如圖 1 所示)中,Native 調用 javascript 代碼比較簡單,直接使用 UIWebView 提供的接口 stringByEvaluatingJavaScriptFromString:就可以實現。而 JavaScript 調用 Native 的功能需要通過攔截請求的方式來實現。即 JavaScript 發送一個特殊的 URL 請求,該請求並不是真正的網絡訪問請求,而是調用 Native 功能的請求,並傳遞相關的參數。Native 端收到請求後進行判斷,如果是功能調 URL 請求則調用 Native 的相應功能,而不進行網絡訪問。

58 同城 iOS 客戶端 Hybrid 框架探索

圖 1 傳統的通訊方式流程

按照上面的思路,在實現 Hybrid 通訊時,我們需要考慮以下幾個問題:

通訊方式

前端能發起請求的方法有很多種,比如使用 window.open()方法、AJAX 請求、構造 iframe 等,甚至於使用 img 標籤的 src 屬性也可以發起請求。58 App 最早是使用 AJAX 請求來發起 Native 調用的,這種方式在最初支撐了 58 App 中 Hybrid 很長一段時間,不過卻存在兩個很嚴重的缺陷:

  • 一是內存問題:在 iOS 8 以前,iOS 中內嵌 Web 頁都是通過系統提供的 UIWebView 來實現的。而在 UIWebView 中,JavaScript 在創建 XMLHttpRequest 對象發起 AJAX 請求後,會存在內存洩露問題。在實現的應用中,JavaScript 與 Native 的交互操作是很頻繁的,使用 XMLHttpRequest 會引起比較嚴重的內存問題。

  • 二是攔截方法:UIWebView 中的正常 URL 請求會觸發其代理方法,我們可以在其代理方法中進行攔截。但是 AJAX 請求是一個異步的數據請求,並不會觸發 UIWebView 的代理方法。我們需要自定義 App 中的 NSURLCache 或 NSURLProcotol 對象,在其中可以攔截到 URL 請求。但是這種方式有兩個問題,一個是當收到功能調用請求時,不易確定是哪個 Web View 對象發起的調用,回調時也無法確定調用哪個 Web View 的回調方法。為了解決這個問題,58 App 的 Hybrid 框架維護了一個 Web View 棧,記錄所有視圖層中的 Web View,前端在發起 Native 調用時,附加一個 Web View 的唯一標識信息。在 Native 需要回調 JavaScript 方法時,通過 Web View 的唯一標識信息在 Web View 棧中找到對應的 Web View。另一個是對 App 的框架結構有影響,Hybrid 中的一個簡單的調用需要放在 App 的全局對象進行攔截處理,破壞 Hybrid 框架的內聚性,違反面向對象設計原則。

iframe 稱作嵌入式框架,和框架網頁類似,它可以把一個網頁的框架和內容嵌入在現有的網頁中。iframe 是在現有的網頁中增加一個可以單獨載入網頁的窗口,通過在 HTML 頁面中創建大小為 0 的 iframe,可以達到在用戶完全無感知的情況下發起請求的目的。使用 iframe 發送請求的代碼如下:

var iframe = document.createElement("iframe");
//設置 iframe 加載的頁面鏈接
iframe.src = “ http://127.0.0.1/NativeFunction?parameters=values”;
//向 DOM tree 中添加 iframe 元素,以觸發請求
document.body.AppendChild(iframe);
//請求觸發後,移除 iframe
iframe.parentNode.removeChild(iframe);

iframe 是加載一個新的頁面,請求會走 UIWebView 的代理方法,不存在 AJAX 請求中無法確定 Web View 的問題。經過調研測試,多次創建和釋放 iframe 不會存在內存洩露的問題。從這兩個方面來說,使用 iframe 是遠優於使用 AJAX 的,比較有名的 PhoneGap 和 WebViewJavascriptBridge 底層都是採用的 iframe 進行通訊的。

iframe 是前端調用 Native 方法的一個非常優秀的方案,但它也存在一些細微的侷限性。58 App 前端為了提升代碼的複用性和方便使用 Native 的功能,對 iframe 的通訊方式進行了統一封裝,封裝的具體實現是——在 JavaScript 代碼中動態地向 DOM tree 上添加一個大小為 0 的 iframe,在請求發起後立刻將其移除。這個操作的前提是 DOM tree 已經形成,也就是說在 DOM Tree 進行之前,這個方案是行不通的。瀏覽器解析 HTML 的詳細過程為:

  1. 接受網絡數據;

  2. 將二進制碼變成字符;

  3. 將字符變為 Unicode code points;

  4. Tokenizer;

  5. Tree Constructor;

  6. DOM Ready;

  7. Window Ready。

Dom Ready 事件就是 DOM Tree 創建完成後觸發的。在業務開發過程中,有少量比較特殊的需求,需要在 DOM Ready 事件之前發起 Native 功能的調用,而動態添加 iframe 的方法並不能滿足這種需求。為此,我們對其他幾種發起請求的方法進行了調查,包括前文提到的 AJAX 請求、為 window.location.href 賦值、使用 img 標籤的 src 屬性、調用 window.open()方法(各個方式的表現結果如表 1 所示)。

58 同城 iOS 客戶端 Hybrid 框架探索

表 1 五種方法效果對比

結果顯示,其他幾種方式除 window.open()與 iframe 表現基本相同外,都有比較致命的缺陷。AJAX 有內存問題,並且無法使用 Web View 代理攔截請求,window.location.href 在連續賦值時只有一次生效,img 標籤不需要添加到 DOM Tree 上也可發起請求,但是無法使用 Web View 代理攔截,並且相同的 URL 請求只發一次。

對於在 DOM Ready 之前需要發起 Native 調用的問題,最終採取的解決方案是儘量避免這種需求。無法避免的進行特殊處理,通過在 HTML 中添加靜態的 iframe 來解決。

通訊協議

通訊協議是整個 Hybrid 通訊框架的靈魂,直接影響著 Hybrid 框架結構和整個 Hybrid 的擴展性。為了保證儘量高的擴展性,58 App 中採用了字典的格式來傳遞參數。一個完整的 Native 功能調用的 URL 如下:

“Hybrid://iframe?parameter={“action”:”changetitle”,”title”:”標題”}11

其中“Hybrid”是 Native 調用的標識,Native 端在攔截到請求後判斷請求 URL 的前綴是否為“Hybrid”,如果是則調起 Native 功能,同時阻止該請求繼續進行。Native 功能調用的相應參數在 parameter 後面的 JSON 數據裡,其中“action”字段指明調用哪個 Native 功能,其餘字段是調用該功能需要的參數。因為“action”字段名稱的原因,後來把為 Web 提供的 Native 功能的處理邏輯稱為 action 處理。

這樣制定通訊協議有很強的可擴展性,Native 端任意增加新的 Hybrid 接口,只要為 action 字段定一個新值,就可以實現,新接口需要的參數完全自定義。但是這種靈活的協議格式存在一個問題,就是開發者很難記住每種調用協議的參數字段,開發過程中需要查看文檔來調用 Native 功能,需要更長的開發時間。為此 58 App 首先建立了健全的協議文檔,將每種調用協議都一一列舉,並給出調用示例,方便前端開發者查閱。另外,Native 端開發了一套協議數據校驗系統,該系統將每種調用協議的參數要求用 XML 文檔表示出來,在收到 Native 調用協議數據時,動態地解析數據內部是否符合 XML 文檔中的要求,如果不符合則禁止調用 Native 功能,並提示哪裡不符合要求。

框架設計

依照上面的通訊協議,58 App 中目前的 Hybrid 的框架設計如圖 2 所示。其中:

58 同城 iOS 客戶端 Hybrid 框架探索

圖 2 Hybrid 框架設計

Native 基礎服務是 Native 端已有的一些通用的組件或接口,在 Native 端各處都在調用,比如埋點系統、統一跳轉及全局 alert 提示框等。這些功能在某些 Web 頁面也會需要使用到。

Native Hybrid 框架是整個 Hybrid 的核心部分,其內部封裝了除緩存以外的所有 Hybrid 相關的功能。Native Hybrid 框架可大致分為 Web 載體、Hybrid 處理引擎、Hybrid 功能接口三部分。校驗系統是前文提到的在開發過程中校驗協議數據格式的模塊,方便前端開發者在開發過程中快速定位問題。

Web 載體包含 Web 載體頁和 Web View 組件,所有的 Hybrid 頁面使用統一的 Web 載體頁。Web 載體頁提供了所有 Web 頁面都可能會使用到的功能,而 Web View 組件為了實現 Web View 的一些定製需求,對系統的 Web View 進行了繼承,並重寫了某些父類方法。

Hybrid 處理引擎負責處理 Web 頁面發起事件,是 Web View 組件的代理對象,也是 Web 調用 Native 功能的通訊橋樑。前面提到的判斷 Web 請求是頁面載入請求還是 Native 功能調用請求的邏輯在 Hybrid 處理引擎中實現。在判定請求為 Native 功能調用請求後,Hybrid 處理引擎根據請求參數中的“action”字段的值對該 Native 調用請求進行分發,找到對應的 Hybrid 功能組件,並將參數傳遞給該組件,由組件進行真正的處理。

Hybrid 功能組件部分包含了所有開放給前端調用的功能。這些功能可以分成兩類,一類是需要 Native 基礎服務支撐的,另一類是 Hybrid 框架內部可以處理的。需要 Native 基礎服務支撐的功能,如埋點、統一跳轉、Native 模塊化組件(圖片選擇、登錄等),本身在 Native 端已經有可用的成熟的組件。這些 Hybrid 功能組件所做的事是解析 Web 頁傳遞過來的參數,將參數轉換為 Native 組件可用的數據,並調用相應的 Native 基礎服務,將基礎服務返回的數據轉換格式回調給 Web。另一類 Hybrid 功能組件通常是比較簡單的操作,比如改變 Web 載體頁的標題和導航欄按鈕、刷新或者返回等。這些組件通過代理的方式獲取載體頁和 Web View 對象,對其進行相應的操作。

再看 Web 端,前端對 Hybrid 通訊進行了一層封裝,將發送 Native 調用請求的邏輯統一封裝為一個方法,業務層需要調用 Native 功能時調用這個方法,傳入 action 名稱、參數,即可完成調用。當需要回調時,需要先定義一個回調方法,然後在參數中將方法名帶上即可。

緩存

Web 頁面具有實時更新的特點,它為 App 提供了不依賴發版就能更新的能力。但是每次都請求完整的頁面,增加了流量的消耗,並且界面展示依賴網絡,需要更長的時間來加載,給用戶比較差的體驗。所以對一些常用的不需要每次都更新的內容進行緩存是很重要的。另外,Web 頁面需要用到的某些 CSS 和 JavaScript 資源是固定不變的,可以直接內置到 App 包中。所以,在 Hybrid 中,緩存是必不可少的功能。要實現 Hybrid 緩存,需要考慮三個方面的問題,即 Hybrid 緩存實現原理、緩存策略和 Hybrid 緩存框架設計。

緩存實現原理

NSURLCache 是 iOS 系統提供的一個類,每個 App 都存在一個 NSURLCache 的單例對象,即使開發者沒有添加任何有關 NSURLCache 的代碼,系統也會為 App 創建一個默認的 NSURLCache 單例對象。幾乎 App 中的所有網絡請求都會調用這個單例對象的 cachedResponseForRequest:方法。該方法是系統從緩存中獲取數據的方法,如果緩存中有數據,通過這個方法將緩存數據返回給請求者即可,不必發送網絡請求。通過使用 NSURLCache 的自定義子類替換默認的全局 NSURLCache 單例,並重寫 cachedResponseForRequest:方法,可以截獲 App 內幾乎所有的網絡請求,並決定是否使用緩存數據。

當沒有緩存可用時,我們在 cachedResponseForRequest:方法中返回 null。這時系統會發起網絡請求,拿到請求數據後,系統會調用 NSURLCache 實例的 storeCachedResponse:forRequest:方法,將請求信息和請求得到的數據傳入這個方法。App 通過重寫這個方法就可以達到更新緩存的目的。

58 App 目前就是通過替換全局的 NSURLCache 對象,來實現攔截 App 內的 URL 請求。在自定義 NSURLCache 對象的 cachedResponse ForRequest:方法中判斷請求的 URL 是否有對應的緩存,如果有緩存則返回緩存數據,沒有則再正常走網絡請求。請求完成後在 store CachedResponse:forRequest:方法中將請求到的數據按需加入緩存中。

使用替換 NSURLCache 的方法時需要注意替換 NSURLCache 單例對象的時機,一定要在整個 App 發起任何網絡請求之前替換。一旦 App 有了網絡請求行為,NSURLCache 單例對象就確定了,再去改變是無效的。

緩存策略

Web 的大部分內容是多變的,開發者需要根據具體的業務需求制定緩存策略。好的緩存策略可以在很大程度上彌補 Web 頁帶來的加載慢和流量耗費大的問題。緩存策略的一般思路是:

  1. 內置通用的資源和關鍵頁面;

  2. 按需緩存常用頁面;

  3. 為緩存設置版本號,根據版本號進行使用和升級。

58 App 中對一些通用資源和十分重要的 Web 頁面進行了內置,防止 App 在首次啟動時由於網絡原因導致某些重要頁面無法展示。在緩存使用和升級的策略上,58 App 除了設置版本號以外,還針對那些已過期但還可用的緩存數據設置了緩存過期閾值。58 App 的詳細緩存策略如下:

  1. 將通用 Hybrid 資源(CSS、JS 文件等)和關鍵頁面(比如業務線大類頁)附帶版本號內置到 App 的特定 Bundle 中;

  2. 在 NSURLCache 單例中攔截到請求後,判斷該請求是否帶有緩存版本號信息,如果沒有,說明該頁面不使用緩存,走正常網絡請求;

  3. 從緩存庫中查找緩存數據,如果有則取出,否則到內置資源中取。如果兩者都沒有數據,走正常網絡請求。並在請求完成後,將結果保存到緩存庫中;

  4. 拿到緩存或內置數據後,將請求中帶的版本號 v1 與取到數據的版本號 v2 進行對比。如果 v1≤v2,返回取到的數據,不再請求網絡;如果 v1>v2 且 v1 – v2 小於緩存過期閾值,則先返回緩存數據以供使用,然後後臺請求新的數據並存入緩存;如果 v1>v2 且 v1 – v2 大於緩存過期閾值,走正常網絡請求,並在請求完成後,將結果保存到緩存庫中。

緩存框架設計

58 App 中 Hybrid 的緩存框架設計如圖 3 所示,其中:

58 同城 iOS 客戶端 Hybrid 框架探索

圖 3 Hybrid 緩存框架設計

1. Hybrid 內置資源管理

Hybrid 內置資源管理模塊是單獨為 Hybrid 的內置資源而創建的。Hybrid 內置資源單獨存放在一個 Bundle 下,這些內置資源主要包括 HTML 文件、JavaScript 文件、CSS 文件和圖片。Hybrid 內置資源管理模塊負責解讀這個 Bundle,並向上提供讀取內置資源的接口,該接口以資源的 URL 和版本號為參數,按照固定的規則進行轉換,查找可用的內置資源。

內置資源中除了這些 Web 資源外,還單獨內置了一份文件,用於保存 URL 到內置資源文件名和內置資源版本號的映射表。管理模塊在收到內置資源請求後,先用 URL 到這個映射表中查找內置資源版本號,比對版本號,然後再通過映射表中查到的文件名讀取相應的內置資源並返回。

2. App 緩存庫

58 App 內有一個獨立的緩存庫組件,App 中需要用到的緩存性質的數據都存放在這個庫中,便於緩存的統一管理。緩存庫內的緩存數據也有版本號的概念,完全可以滿足 Hybrid 緩存的需求,且使用十分方便。Hybrid 的緩存數據都使用 App 的緩存庫來保存。

3. Hybrid 緩存管理器

Hybrid 緩存管理器是 Hybrid 緩存相關功能的總入口,負責提供 Hybrid 緩存數據和升級緩存數據,所有的 Hybrid 緩存相關的策略都封裝在這個模塊中。全局的 NSURLCache 實例在收到 Hybrid 請求時會調起 Hybrid 緩存管理器,索取緩存數據。Hybrid 緩存管理器先到 App 的緩存庫中查找可用的緩存,如果沒有再到內置資源管理模塊查找,如果可以查到數據,則返回查到的數據,如果查不到,則返回空。在 NSURLCache 的 storeCachedResponse:forRequest:方法中,會調用 Hybrid 緩存管理器的緩存升級接口,將請求到的數據傳入該接口。新請求到的數據會帶有最新的版本號信息。緩存升級接口將新的數據和版本號信息一同存入緩存庫中,以便下次使用。

性能

前面分享了 58 App 中 Hybrid 的通訊框架和緩存框架,接下來介紹一下遇到的性能方面的問題及解決方案。

AJAX 通訊方式的內存洩露問題

前面介紹過在 UIWebView 中使用 AJAX 的方式進行 Native 功能調用,會產生內存洩露問題,《UIWebView Secrets - Part1 - Memory Leaks on Xmlhttprequest》(參考資料 1)中給出了一個解決方案,是在 UIWebView 的代理方法 WebViewDidFinishLoad:中添加如下代碼:

[[NSUserDefaults standardUserDefaults] setInteger:0 forKey:@"WebKitCacheModelPreferenceKey"];11

測試結果顯示,這種方法並沒有使用 iframe 的效果好。加上攔截方式的侷限性,58 App 最終選擇的解決方案是使用 iframe 代替 AJAX。

UIWebView 內存問題

使用過 UIWebView 的開發者應該都知道,UIWebView 有比較嚴重的內存問題。蘋果在 iOS8 推出了 WebKit 框架,其核心是 WKWebView,志在取代 UIWebView。WKWebView 不僅解決了 UIWebView 的內存問題,且具有更高的穩定性和響應速度,還支持一些新的功能。使用 WKWebView 代替 UIWebView 對提升整個 Hybrid 框架的性能會有很重大的意義。

但是,WKWebView 一直存在一個問題,就是 WKWebView 發出的請求並不走 NSURLCache 的方法。這就導致我們自定義的緩存系統會整個失效,也無法再用內置資源。經過一段時間的摸索和調研,終於找到了可以實現自定義緩存的方法。主要思想是 WKWebView 發起的請求可以通過 NSURLProtocol 來攔截——將自定義的 NSURLProtocol 子類註冊到 NSURLProtocol 的方式,可以像之前用 NSURLCache 一樣使用緩存或內置數據代替請求結果返回。註冊自定義 NSURLProtocol 的關鍵代碼如下:

[NSURLProtocol registerClass:WBCustomProtocol.class];
Class cls = NSClassFromString(@"WKBrowsingContextController");
SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");
if ([(id)cls respondsToSelector:sel]) {

代碼中從第二行開始,是為了讓 WKWebView 發起的請求可以被自定義的 NSURLProtocol 對象攔截而添加的。添加了上面的代碼後,就可以在自定義的 NSURLProtocol 子類的方法中截獲到 WKWebView 的請求和數據下載完成的事件。

以上方案解決了 WKWebView 無法使用自定義緩存的問題,但是這種方案還存在一些問題,且使用了蘋果系統的私有 API,不符合官方規定,在 App 中直接使用有被拒的風險。另外 WKWebView 還有一些其他問題(詳情可參見參考資源 6)。

目前,58 App 正在準備接入 WKWebView,但是沒有決定使用這種方案來解決自定義緩存問題。我們正在逐步減少對自定義緩存的依賴程度,在前面幾個版本迭代中,已經逐步去除了內置的 HTML 頁面。

頁面加載完成事件優化

正常的 Web 頁面加載是比較耗時的,尤其是在網絡環境較差的情況下。而 Web 的頁面文件與樣式表、JavaScript 文件以及圖片是分別加載的,很有可能界面元素已經渲染完成,但樣式表或 JavaScript 文件還沒有加載完,這時會出現佈局混亂和事件不響應的情況,影響用戶體驗。為了不讓用戶看到這種情況,一般 Native 會在加載 Web 資源的過程中隱藏掉 Web View,或用 Loading 視圖遮擋住 Web View。等到 Web 資源加載完成再將 Web View 展示給用戶。系統通過 UIWebViewDelegate 的 WebViewDidFinishLoad:方法告知 Native 資源加載完成的事件。這個方法在頁面用到的所有資源文件全部加載完成後觸發。

在實用中發現,一般情況下樣式表資源和 JavaScript 資源的加載速度很快,比較耗時的是圖片資源(事實是 Native 界面也存在圖片加載比較慢的情況,一般 Native 會採用異步加載圖片的策略,即先將界面展示給用戶,後臺下載圖片,下載完成後再刷新圖片控件)。實際上當 HTML、樣式表和 JavaScript 文件加載完成後,整個界面就完全可以展示給用戶並允許用戶交互了。圖片資源加載完成與否並不影響交互。

且這樣的邏輯也與 Native 異步加載圖片的體驗一致。在 WebViewDidFinishLoad:方法中才展示界面的策略會延長加載時間,尤其在圖片很大或網絡環境較差的情況下,用戶可能需要多等待幾倍的時間。

基於以上的考慮,58 App 的 Hybrid 框架專門為 Web 提供了一功能接口,允許 Web 提前通知 Native 展示界面。該功能實現起來很簡單,只需單獨定義一個 Hybrid 通訊協議,並在 Native 端相應的處理邏輯即可。前端在開發一些圖片資源比較多的頁面時,提前調用該接口,可以在很大程度上提升用戶體驗。

耦合

58 App 最初引入 Hybrid 的時候,業務要簡單許多,Native 沒有現在這麼多功能可供 Web 調用,所以最開始設計的 Hybrid 通訊框架也比較簡單。由於使用 AJAX 的方式進行通訊,通訊請求的攔截也要在 NSURLCache 中。當時也沒有公用的緩存庫組件,Hybrid 的緩存功能與內置資源一起寫在單獨的模塊中(最初的 Hybrid 框架如圖 4 所示)。

58 同城 iOS 客戶端 Hybrid 框架探索

圖 4 舊版 Hybrid 框架設計圖

這個框架在 58 App 中存在了很長一段時間,運行比較穩定。但是隨著業務的不斷增加,這個框架暴露出了一些比較嚴重的問題。

自定義的 NSURLCache 類中耦合了 Hybrid 的業務邏輯

由於 AJAX 方式的通訊請求要在 NSURLCache 中進行攔截,NSURLCache 在收到請求後,不得不先判斷是否是 Hybrid 通訊請求——如果是,則需要將請求轉發給 Hybrid 通訊框架處理。另外,為了解決 Native 回調 Web 時無法確定 Web View 的問題,需要維護一個 Web View 的 Web View 棧,App 內所有的 Web View 對象都需要存入到這個棧中。這個棧需要全局存放,但是 Web 載體頁和 Hybrid 事件分發器都是局部對象,無法保存這個棧。考慮到 NSURLCache 對象與 Hybrid 有關聯且是單例,最終將這個棧保存在了 NSURLCache 的屬性中,更加重了 NSURLCache 與 Hybrid 的耦合。

NSURLCache 耦合 Hybrid 業務邏輯的問題隨著 iframe 的引入迎刃而解,通訊請求的攔截直接轉移到了 Hybrid 事件分發器中。

NSURLCache 的職責重新恢復單一,只負責緩存相關的內容。使用 iframe 的通訊方式,Web 在調用 Native 功能的請求是在 UIWebView 的代理方法中截獲,系統會將相應的 Web view 通過參數傳遞過來,不再有無法確定 Web view 的問題,之前的 Web view 棧也沒有必要再維護了。iframe 的引入使得 Hybrid 的通訊框架和緩存框架完全分離開來,互不干涉。

Web 載體頁臃腫

最初的 Hybrid 框架中,action 處理的具體實現寫在了 Web 載體頁中,這導致 Web 載體頁隨著業務的增加變得十分臃腫,內部包含大量的 action 處理代碼。另外,由於一些為 Web 提供的功能是針對某些特定業務場景的,寫在公用載體頁中並不合適,所以開始了使用繼承的方式派生出各種各樣的 Web 載體頁,最終導致 App 內的 View Controller 的繼承關係十分混亂,繼承層次最多時高達九層。

Web 載體頁耦合 action 處理的問題是業務逐步累積的結果,當決定要重構的時候,這裡的邏輯已經變得十分龐雜。強行將這兩部分剝離困難很大,一方面代碼太多,工作量大,另一方面邏輯過於複雜,稍有不慎就會引起 Bug。解決 Web 載體頁的問題採取的方案分成兩部分。

搭建新 Hybrid 框架,逐步淘汰老的框架。

為了解決 Web 載體頁臃腫的問題,更為了提供對 iOS 8 WebKit 框架的支持,提升 Hybrid 性能,58 iOS 客戶端重新搭建了一套新的 Hybrid 框架。新 Hybrid 框架嚴格按照圖 2 所示的結構進行實現。新增的業務使用新的 Hybrid 框架,並逐步將老的業務切換到新的框架上來。

在圖 2 的框架中,為了在增加新的 Hybrid 功能組件時整體框架滿足開閉原則,需要解除 Hybrid 處理引擎對 Hybrid 功能組件的依賴。這裡採用的設計是,處理引擎不主動添加組件,而是提供全局的註冊接口,內部保存一份共享的註冊表。各個功能組件在 load 方法中主動向處理引擎中註冊 action 名稱、功能組件的類名及方法。處理引擎在運行時動態地查閱註冊表,找到 action 對應的類名和方法,生成功能組件的實例,並調用相應的處理方法。

按照上面的設計,一個 Web 界面的完整運行流程為:

  1. 程序開始運行,生成全局的 Hybrid 共享註冊表(action 名稱到類名及方法名的映射),各個 Hybrid 功能組件向註冊表中註冊 action 名稱;

  2. 需要使用 Web 頁,應用程序生成 Web 載體頁;

  3. Web 載體頁生成 Web View 實例和 Hybrid 處理引擎實例,並強持有這兩個實例,將處理引擎實例設為 Web view 實例的代理對象,將自身設為處理引擎的代理對象;

  4. Web 頁發起 Native 調用請求;

  5. 處理引擎實例截獲 Native 調用請求,並在共享註冊表中查到可以處理本次請求的類名和方法名;

  6. 處理引擎生成查找到的 Hybrid 功能組件類的實例,強持有之,並將自身的代理對象設為功能組件的代理對象,調用該實例的處理方法;

  7. Hybrid 功能組件解析全部的調用參數,處理請求,並通過代理對象將處理結果回調給 Web 頁。

  8. Web 頁生命週期完成,釋放 Web View 實例、Hybrid 處理引擎實例、Hybrid 引擎實例釋放所有的 Hybrid 功能組件實例。

通過使用組件主動註冊和運行時動態查找的方式,固化了新增組件的流程,保證已有代碼的完備性,使 Hybrid 框架在增加新的功能上嚴格遵守開閉原則。

關於註冊表,目前是採用全局共享的方式保存。在最初設計時,還有另一種動態組合註冊的方案。該方案不使用共享的註冊表,而是每一個 Hybrid 處理引擎保存一份獨立的註冊表,在 Web 載體頁生成 Hybrid 處理引擎的時候,根據業務場景選擇部分 Hybrid 功能組件註冊到處理引擎中。這種動態組合的方案對功能組件的組合進行了細化,每個 Web 載體頁對象根據各自的業務場景按需註冊組件。動態組合註冊的方案考慮的主要問題是:在 Hybrid 框架中,有許多專用 Hybrid 功能組件,大部分 Web 頁並不需要使用這些組件,另外 58 App 被拆分為主 App 和多個業務線共同維護和開發,有一些 Hybrid 功能組件是業務線獨有的,其他業務線並不需要使用。動態組合註冊的方案可以達到隔離業務線的目的,同時不使用全局註冊表,在不使用 Web 頁時不佔用內存資源,也減小了單張註冊表的大小。

現在的 Hybrid 框架採用全局註冊方案,而沒有采用動態組合註冊的方案,原因是動態組合註冊方案需要在生成 Web 載體頁時區分業務場景,Web 頁的使用方必須提供需要註冊的組件信息,而這是比較困難的,也加大了調用方調用 Web 頁的複雜程度。另外,大部分組件是否會被使用都是處於模糊狀態,並不能保證使用或者不使用,這種模糊性越大,使用動態組合註冊方案的意義也就越小。

最終 58 App 採用了全局註冊的方案,雖然註冊表體積較大,但是由於使用散列算法,並不會增加查找的複雜度而影響性能,同時避免了調用方需要區分業務場景的不便,簡化了後續的開發成本。

改造原 Hybrid 框架,防止 Web 載體頁進一步擴大

為了保證業務邏輯的穩定,不能直接淘汰老的 Hybrid 框架,老業務中會有一部分新的需求需要在老的框架上繼續擴展。為了防止老的 Web 載體頁因為這些新需求進一步擴大,決定將原 Hybrid 通訊框架改裝為雙向支持的結構。在保持原 Web 功能接口處理邏輯不變的情況下,支持以組件的方式新增 Web 功能接口。具體的實現是在 Hybrid 事件分發器中也添加了與新 Hybrid 框架的處理引擎相似的邏輯,增加了全局共享註冊表,支持組件向其中註冊。在分發處理中添加了查找和調用註冊組件的邏輯。改造後的 Hybrid 事件分發器在收到 action 請求後,先按老的邏輯進行分發,如果分發成功則調用載體頁的處理邏輯,如果分發失敗,則查找共享註冊表,找到可以處理該 action 的組件進行實例化,並調用相應的處理邏輯。

雖然 Web 載體頁由於繼承的關係變得很分散,但是事件分發器一直只有一份,邏輯比較集中。進了這樣的改造後,有效扼制了 Web 載體的進一步擴大,也不再需要使用繼承來複用 action 處理邏輯了。

總結

本文重點介紹了 58 App 中 Hybrid 框架在設計和發展過程中遇到的問題及採用的解決方案。目前的 Hybrid 框架是一個比較簡單實用的框架,前端沒有對 Native 提供的功能進行一一封裝,這樣可以在擴展新 action 協議時儘量少地改動代碼。且封裝層次少,執行效率比較高。目前的 Hybrid 框架依然很友好地支撐著 58 業務的發展,所以暫時還沒引入 JavaScriptCore.framework。在未來的發展中,會逐步引入新技術,搭建更好的 Hybrid。


暑期互聯網遊學夏令營活動:http://www.ujiuye.com/zt/sqxly/?wt.bd=zt36716tt

2017大學生就業扶助基金:http://www.ujiuye.com/zt/jyfc/?wt.bd=zt36716tt

IT學習、就業交流互動群:http://www.ujiuye.com/zt/qqhdjlpt/?wt.bd=zt36716tt

相關推薦

推薦中...