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

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

由于 XMLHttpRequest 的方式是進(jìn)行頁面局部刷新,并不能被 UIWebViewDelegate 代理的 - (BOOL)webView:(UIWebView *)webView shouldStartL
OAdWithRequest:(NSURLRequest *)request navigationType:
(UIWebViewNavigationType)navigationType 方法攔截到,設(shè)計到這里又出現(xiàn)了新問題,如何讓 Native 能攔截到 AJAX 請求呢?
經(jīng)過一番調(diào)研,我們找到了用于緩存的 NSURLCache,對于 UIWebView 中的所有請求(包括 AJAX 請求)都會走 NSURLCache。因此,我們決定采用復(fù)用緩存中的 WBHybridComponent 攔截 AJAX 請求,具體 Web 調(diào) Native 的交互設(shè)計如圖 5 所示。
圖5 Hybrid 框架處理流程圖
其中,H5ViewController 為 HTML5 的載體頁,WBWebView 是 UIWebView 派生類。WBWebView 中通過 AJAX 發(fā)出的異步請求,在 WBHybridComponent 中被攔截,再通過 WBHybridJSHandler 中的 dic 表找到對應(yīng)的 WBActionAnalysis 對象,然后在 WBActionAnalysis 中分析異步請求傳過來的協(xié)議,取出 action 字段,再根據(jù) action 值找到 delegate 即 H5ViewController 中對應(yīng)的方法。
AJAX 發(fā)出的請求我們約定為:nativechannel://?paras=<json 協(xié)議>,WBHybridComponent 在攔截時判斷 URL 中是否為 nativechannel 的協(xié)議頭,如果是則為 Web 調(diào)起 Native 操作,需要進(jìn)行后續(xù) Native 處理;否則放過進(jìn)行其他處理。<json 協(xié)議> 的簡化格式如圖 6 所示,這是二手車大類頁點(diǎn)擊二手車類目 Web 調(diào) Native 時 AJAX 傳過來的協(xié)議。
圖6 Web 調(diào) Native 傳輸協(xié)議
改進(jìn)的 Hybrid 框架
前面我們設(shè)計的 Hybrid 框架,通過創(chuàng)建 XMLHttpRequest 對象發(fā)送 AJAX 請求的方式能達(dá)到 Web 調(diào) Native 的目的,也可以滿足業(yè)務(wù)上的需求,在一段內(nèi)發(fā)揮了重要作用。但隨著時間的推移,這個 Hybrid 框架暴露出了一些問題,如下所示。
1.我們發(fā)現(xiàn) App 中存在大量的內(nèi)存泄露,經(jīng)查罪魁禍?zhǔn)拙故?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 交互的時候維護(hù)了一個

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