第一版 App 架構
早在 2010 年 58 同城誕生第一版 iOS 客戶端,按照傳統的 MVC 模式去設計,純 Native 頁面,這時的功能較為簡單,架構也是如此,從上至下分為 UI 展現、業務邏輯、數據訪問三層,如圖 1 所示。和同期其他公司一樣,App 的出發點是為了快速搶占市場,采取“短平快”的方式開發。純 Native 的 App 在早期業務量不是太大的情況下,能滿足業務的需求。
圖1 App 早期架構
第二版架構
Hybrid 框架需求
由于蘋果審核周期較長,業務需求不斷增大,有些業務如果用 Native 進行開發,工作量大投入人員較多,也不能動態更新,如 58 App 的大類、列表、詳情頁面。這種情況下,用 HTML5 是比較流行的解決方式,由此產生了第二版架構,如圖 2 所示,在 UI 層添加了 HTML5 頁面及 Hybrid 交互框架。
圖2 帶 Hybrid 的架構
當時 58 App 設計時用于加載 HTML5 的組件是 UIWebView,也只能使用這個(彼時還沒有 WKWebView),但實現起來有幾個問題是需要解決的:
怎么解決 Hybrid 中 Web 和 Native 交互問題,如用戶點擊一個類別,能調起 Native 的一些方法去執行相關頁面跳轉或寫日志。
如何提高 HTML5 頁面的加載速度,HTML5 頁面加載時要下載一些 JavaScript、CSS 及圖片資源,是比較耗時的。
設置緩存
為了方便描述,本文先介紹如何提高 HTML5 頁面加載速度的問題。
對于一些訪問比較頻繁的頁面,如大類列表詳情,我們早期采用的都是 HTML5 頁面。要加速這些頁面的渲染,就要想辦法提升資源的加載。那么如何實現呢?首先想到的是使用緩存,我們可以把這些頁面的資源內置到 App 中隨版本發布。
由于 UIWebView 在發請求的時候都會走 NSURLCache 的這個方法:
- (nullable NSCachedURLResponse*)cachedResponseForRequest:(NSURLRequest *)request;
我們可以從 NSURLCache 派生出子類 WBHybrid Component,復寫 cachedResponseForRequest:方法,在這之中加載 App 的內置資源,具體加載策略可見圖 3。
圖3 緩存處理流程
其中,H5ViewController 為 HTML5 載體頁面,WBCacheHandler 為專門處理內置資源類,用于加載、查找、下載、保存內置資源。URL 的 query 中設置版本號參數 cachevers 作為資源緩存的標識,其值為數字類型,假設 cachev1,其與內置資源中的版本號如為 cachev2 進行對比,若 cachev2>= cachev1,表示內置資源中是最新數據,直接給請求返回數據;否則下載新的內置資源,同時根據 cachev1- cachev2 的差值進行判斷,如設置一個臨界值 x,若差值大于 x,則說明內置資源為舊,給請求返回 nil,否則返回內置數據,讓請求先用緩存數據,下次啟動時再用新數據。
內置數據采用的是一個 bundle 包,如圖 4 所示,CacheResources.bundle 為內置包名,里面包含了一個索引文件和若干個內置數據文件,其中索引文件中每項 item 格式為 key、版本號和文件名。
圖4 緩存包結構
想要使用自定義的 NSURLCache,必須在 App 啟動時初始化 WBHybridComponent,并進行設置,替換默認的 Cache,注意:這個設置必須在所有請求之前進行,否則設置失效,而是采用默認的 NSURLCache 實例,我們曾經踩過這個坑。

基于 AJAX 的 Hybrid 框架
對于前面所列的第一個問題,我們是要設計一個 Web/Native 的 Hybrid 框架。交互主要包括兩部分內容,一是 Native 調用 Web,這個比較簡單,直接通過 UIWebView 的 stringByEvaluatingJavaScriptFromString:執行一段 JS 腳本,并返回執行結果,本文主要分享 Web 調 Native 的方法。
對于 Web 調 Native 交互的方式,我們采用異步 AJAX 進行,創建一個 XMLHttpRequest 對象,執行 send()進行異步請求,Native 攔截。

由于 XMLHttpRequest 的方式是進行頁面局部刷新,并不能被 UIWebViewDelegate 代理的 - (BOOL)webView:(UIWebView *)webView shouldStartL
OAdWithRequest:(NSURLRequest *)request navigationType:
(UIWebViewNavigationType)navigationType 方法攔截到,設計到這里又出現了新問題,如何讓 Native 能攔截到 AJAX 請求呢?
經過一番調研,我們找到了用于緩存的 NSURLCache,對于 UIWebView 中的所有請求(包括 AJAX 請求)都會走 NSURLCache。因此,我們決定采用復用緩存中的 WBHybridComponent 攔截 AJAX 請求,具體 Web 調 Native 的交互設計如圖 5 所示。
圖5 Hybrid 框架處理流程圖
其中,H5ViewController 為 HTML5 的載體頁,WBWebView 是 UIWebView 派生類。WBWebView 中通過 AJAX 發出的異步請求,在 WBHybridComponent 中被攔截,再通過 WBHybridJSHandler 中的 dic 表找到對應的 WBActionAnalysis 對象,然后在 WBActionAnalysis 中分析異步請求傳過來的協議,取出 action 字段,再根據 action 值找到 delegate 即 H5ViewController 中對應的方法。
AJAX 發出的請求我們約定為:nativechannel://?paras=<json 協議>,WBHybridComponent 在攔截時判斷 URL 中是否為 nativechannel 的協議頭,如果是則為 Web 調起 Native 操作,需要進行后續 Native 處理;否則放過進行其他處理。<json 協議> 的簡化格式如圖 6 所示,這是二手車大類頁點擊二手車類目 Web 調 Native 時 AJAX 傳過來的協議。
圖6 Web 調 Native 傳輸協議
改進的 Hybrid 框架
前面我們設計的 Hybrid 框架,通過創建 XMLHttpRequest 對象發送 AJAX 請求的方式能達到 Web 調 Native 的目的,也可以滿足業務上的需求,在一段內發揮了重要作用。但隨著時間的推移,這個 Hybrid 框架暴露出了一些問題,如下所示。
1.我們發現 App 中存在大量的內存泄露,經查罪魁禍首竟是 UIWebView。調研發現 UIWebView 中執行 XMLHttpRequest 異步請求時會有內存泄露,網上也有人探討過這個問題,參考博文:http://blog.techno-barje.fr//post/2010/10/04/UIWebView-secrets-part1-memory-leaks-on-xmlhttprequest/。
2.Hybrid 交互方式與緩存都使用 NSURLCache 的派生類 WBHybridComponent 執行攔截,其初衷也是用于緩存。我們的 Hybrid 框架將兩者耦合在一起,這對于后期的開發和性能優化工作會帶來不少隱患。
3.我們在 Hybrid 交互的時候維護了一個

由于 iframe 方式是整個頁面刷新,所以能執行 UIWebViewDelegate 的回調方法 - (BOOL)webView:(UIWebView )webView shouldStartLoadWithRequest:(NSURLRequest )request navigationType:(UIWebViewNavigationType)navigationType。我們可以直接在這個方法中攔截 Web 的調起,iframe 方式處理流程如圖 7 所示。
圖7 iframe 的 Hybrid 交互方式
通過 iframe 的方式,我們 App 極大地簡化了 Hybrid 框架的交互流程,同時也解決了內存泄露、與緩存功能耦合、消耗不必要的內存空間等問題。
第三個版本架構
隨著業務的進行,一些新的技術需求來了,比如有些基礎模塊可以從 App 中獨立出來進行多應用間的復用;需要為轉轉 App 提供一個日志 SDK;為違章查詢等 App 提供登錄的 Passport SDK;為其他 App 提供一個可定制化的分享組件等等。
App 拆分組件
這時我們迫切地需要在工程代碼層面對原來的 App 進行拆分、組件化開發,如圖 8 所示。
圖8 第三版架構
我們將 App 拆分成三層,從下至上依次是基礎服務層、基礎業務層、主業務層:
1.基礎服務層里的組件是與業務無關的,供上層調用,每個組件為一個工程,如網絡、數據庫、日志等。這里面有些組件是整個公司的其他 App 也在使用,如樂高日志,我們對外提供一個 SDK,與文檔一起放在代碼服務器上供其他團隊使用。并將 58 App 中用到的所有第三方庫都集中起來存放到一個專門的工程中,也便于更新維護。
2.基礎業務層里的組件是與業務相關的,供主業務層使用,每個組件是一個工程,如登錄、分享、推送、IM 等,我們把 Hybrid 框架也歸在業務層。其中登錄組件我們做成 Passport SDK,供公司其他 App 集成調用。
3.主業務包括 App 首頁、個人中心、各業務線業務和第三方接入業務,業務線業務主要包括發布、大類、列表、詳情。
集成管理組件
工程拆分完后,就是工程集成了,我們用 Cocoapods 將各工程集成到一起編譯運行和打包,對于每一個工程配置好.podspec 文件。在配置 podfile 文件時,當用于本地開發時,我們通過 path 的方式進行集成,不用臨時下載工程代碼,如下所示。
pod proj, :path => '~/58_ios_libs/proj’
在進行 Jenkins 打包時,我們通過 Git 方式將代碼實時下載:
GitLab 服務進行代碼管理
我們在局域網搭建一個 GitLab 服務,用于管理所有工程代碼,并設置好開發組及相應的權限。通過 GitLab 還可以實現提交代碼審核、代碼合并請求及工程分支保護。
第四版架構
隨著 58 App 用戶量的劇增,各業務線業務迅速增長,對 58 App 又提出了新需求,如為加快大類列表詳情頁面的渲染速度,需要將原來這些 HTML5 頁面 Native 化;再如各業務線要定制列表詳情和篩選樣式。面對如此眾多需求,顯然原來的架構已經滿足不了,那就需要我們進一步改進客戶端架構,將主業務層進一步拆分。
主業務層拆分
我們對主業務層進行一個拆分,拆分后的整體架構如圖 9 所示,其中每一個模塊為一個工程,也是一個組件。
圖9 第四版架構
我們將首頁、發布、發現、消息中心、個人中心及第三方業務等都從主業務層拆分出來成為獨立工程。同樣將房產、二手、二手車、黃頁、招聘等業務線的代碼從原工程里面剝離出來,每個業務線獨立一工程,將列表和詳情分別剝離出來并進行 Native 化,為上層業務線定制功能提供接口。
業務線拆分的時候我們遵循以下幾個原則:
1.各業務線之間不能有依賴關系,因為我們的業務線在開發的整個過程中都是獨立運行的,不會含有其他業務線代碼。
2.非業務線工程不能對各業務線有依賴關系,即所有業務線都不集成進 App 也要能正常編譯。
3.各業務線對非業務線工程可以保留必要的依賴,如業務線對列表組件的依賴。
在拆分過程中我們也采取了一些策略,如在拆分招聘業務線時,先把招聘業務線從集成后的工程中刪除,進行編譯,會出現各種編譯錯誤,說明是有工程對招聘業務線代碼進行依賴。如何解決這些依賴關系呢?我們主要是解決相互依賴關系,招聘業務線對非業務線工程肯定是有一定的依賴關系,這個先保留,我們要解決的是其他組件甚至可能是其他業務線對招聘的依賴。我們總結了下,主要用了以下幾種方式:
1.將依賴的文件或方法下沉,如有些文件并不是招聘業務線專用的,可以從招聘中下沉到其他工程,同樣有些方法也可以下沉。
2.Runtime,這種方式比較普遍,但也不需要所有地方都用,畢竟其維護成本還是比較高的。
3.Category 方式,如個人中心組件中方法 funA 要調用招聘組件中的方法 funB,但 funB 的實現是要依賴招聘內部代碼,這種情況下個人中心是依賴招聘業務線的,理論上招聘可以依賴個人中心,而不應該反過來依賴。解決辦法是可以在個人中心添加一個類,如 ClassA,里面添加方法 funB,但實現為空,如果帶返回值可以返回一個默認值,再在招聘中添加一個 ClassA 的類別 ClassA+XX,將原來招聘中的方法 funB 放入 ClassA+XX,這樣如果招聘集成進來,就會執行 ClassA+XX 中的 funB 方法,否則執行個人中心自己的 funB 方法。
跳轉總線
總線包括 UI 總線和服務總線,前者主要處理組件間頁面間的跳轉,尤其是在主業務層,UI 總線用得比較頻繁。服務總線主要處理組件間的服務調用,這里主要講跳轉總線。在主業務層,被封裝成的各個組件需要通過 UI 總線進行頁面跳轉,我們設計了一個總分發中心和子分發中心的模式進行處理,如圖 10 所示。
圖10 UI 跳轉總線
主業務層每個組件內都有一個子分發中心,它的處理邏輯由各組件內來進行,但必須實現一些共同的接口,且這個子分發中心需要進行注冊。當組件內需要進行 UI 跳轉時,調用總分發中心,將跳轉協議傳入總分發中心,總分發中心根據協議中組件標識(如業務線標識)找到對應的目標組件子分發中心,將跳轉協議透傳到對應的子分發中心。接下來的跳轉由子分發中心去完成。這樣的方式極大降低了組件間的耦合度。
UI 總線中的跳轉協議我們原來用 JSON 形式,后來統一調整為 URL 的方式,將 m 調起、瀏覽器調起、push 調起、外部 App 調起和 App 內跳轉統一處理。
新統跳協議 URL 格式如下:
wbmain://jump/job/list? ABMark=markID¶ms=
其中,wbmain 為 58 App 的 scheme,job 為招聘業務線標識,list 為到列表頁,ABMark 為 AB 測跳轉用的標識 ID,后面會細講,params 為傳過來的一些參數,如是否需要動畫,push 還 present 方式入棧等。為了兼容老協議,我們將原來協議中的一部分內容直接透傳到 params 中。
AB 測跳轉
對于指定跳轉 URL,有時跳轉的目標頁面是不固定的,如我們的發布頁面,有 HTML5 和 React Native 兩套頁面,如果 React Native 頁面出了問題,可以將 URL 做修改跳到 HTML5 頁面。具體方案是服務器下發一個路由表,每個表項有一個 ID 和對應新的跳轉 URL,每個表項設置有過期時間。跳轉的 URL 可以帶有 AB 測跳轉用的標識 ID,即 markID。如果有這個標識,跳轉時就去與路由表中的表項匹配,如果命中就改用路由表中的 URL 跳轉,否則還用原來的 URL 執行跳轉,大概流程如圖 11 所示。
圖11 AB 測跳轉流程圖
靜態庫方案
為了提高整個 App 的編譯速度,我們為每個工程配置一個對應的庫工程,里面預先由源碼工程編譯出來一個對應的靜態庫,如圖 12 所示。
圖12 源碼庫與靜態庫對應關系
開發人員可以將權限內的源碼和靜態下載到本地,按需進行源碼和庫混合集成,如對于招聘業務線 RD,我們只需關心招聘業務線源碼工程,不需要其他業務線的源碼或靜態庫,剩下的工程可以選擇全部用靜態庫進行集成。
對于 Jenkins 打包平臺,我們也可以根據需求適當在源碼和靜態庫之間做選擇。對于一些特殊的工程,如第三方庫工程 ThirdComponent,一般也不會變,可以直接接入對應的靜態庫工程 ThirdComponentLib。
總結
業務在不斷變化,需求持續增多,技術也在不斷地更新,我們的架構也需要不斷進行調整和升級,架構的演進是一項長期的任務。
核心關注:拓步ERP系統平臺是覆蓋了眾多的業務領域、行業應用,蘊涵了豐富的ERP管理思想,集成了ERP軟件業務管理理念,功能涉及供應鏈、成本、制造、CRM、HR等眾多業務領域的管理,全面涵蓋了企業關注ERP管理系統的核心領域,是眾多中小企業信息化建設首選的ERP管理軟件信賴品牌。
轉載請注明出處:拓步ERP資訊網http://www.guhuozai8.cn/
本文標題:58 同城 iOS 客戶端組件化演變歷程
本文網址:http://www.guhuozai8.cn/html/consultation/10839320604.html