天天看點

58同城iOS用戶端Hybrid架構探索

作者:杜豔新,劉文軍。58同城iOS進階研發工程師,專注于App Hybrid架構的架構研發,主導了58同城App的Hybird混合研發的系統架構以及研發。

責編:唐小引,歡迎技術投稿、約稿、給文章糾錯,請發送郵件至[email protected]。

本文為《程式員》原創文章,未經允許不得轉載,更多精彩文章請訂閱《程式員》

58同城iOS用戶端的Hybrid架構在最初設計和演進的過程中,遇到了許多問題。為此,整個Hybrid架構産生了很大的變化。本文作者将遇到的典型問題進行了總結,并重點介紹58 iOS采用的解決方案,希望能給讀者搭建自己的Hybrid架構提供一些參考。

引言

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 = null;
           

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”:”标題”}
           

其中“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: forKey:@"WebKitCacheModelPreferenceKey"];
           

測試結果顯示,這種方法并沒有使用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]) {
    [(id)cls performSelector:sel withObject:@"http"];
           

代碼中從第二行開始,是為了讓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。

參考資料

  1. UIWebView Secrets - Part1 - Memory Leaks on Xmlhttpreques
  2. IFrame (Inline Frame)
  3. 《網頁加載曆程詳解》
  4. NSURLCach
  5. 《WKWebView不支援NSURLProtocol》
  6. 《WKWebView那些坑》
58同城iOS用戶端Hybrid架構探索

訂閱程式員(含iOS、Android及印刷版)請通路 http://dingyue.programmer.com.cn

58同城iOS用戶端Hybrid架構探索

訂閱咨詢:

  • 線上咨詢(QQ):2251809102
  • 電話咨詢:010-64351436
  • 更多消息,歡迎關注“程式員編輯部”