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

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

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

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