美團外賣平臺化複用主要是指多端程式碼複用,正如美團外賣iOS多端複用的推動、支撐與思考文章所述,多端包含有兩層意思:其一是相同業務的多入口,指美團外賣業務需要在美團外賣App(下文簡稱外賣App)和美團App外賣頻道(下文簡稱外賣頻道)同時上線;其二是指平臺上各個業務線,美團外賣不同業務線都依賴外賣基礎服務,比如登陸、定位等。
多入口及多業務線給美團外賣平臺化複用帶來了巨大的挑戰,此前我們的一篇部落格《美團外賣Android平臺化架構演進實踐》(下文簡稱《架構演進實踐》)也提到了這個問題,本文將在“程式碼複用”這一章節的基礎上,進一步介紹平臺化複用工作面臨的挑戰以及相應的解決方案。
美團外賣平臺化複用背景
美團外賣App和美團App外賣頻道業務基本一樣,但由於歷史原因,兩端程式碼差異較大,造成同樣的子業務需求在一端上線後,另一端幾乎需要重新實現,嚴重浪費開發資源。在《架構演進實踐》一文中,將美團外賣Android客戶端平臺化架構分為平臺層、業務層和宿主層,我們希望能夠在平臺化架構中實現平臺層和業務層的多端複用,從而節省子業務需求開發資源,實現多端部署。
難點總結
兩端業務雖然基本一致,但是仍舊存在差異,UI、基礎服務、需求差異等。這些差異存在於美團外賣平臺化架構中的平臺層和業務層各個模組中,給平臺化複用帶來了巨大的挑戰。我們總結了兩端程式碼的差異點,主要包括以下幾個方面:
- 基礎服務的差異:包括基礎Activity、網路庫、圖片庫等底層庫的差異。
- 元件的實現差異:包括基礎資料Model、下拉重新整理、頁面跳轉等基礎元件的差異。
- 頁面的差異:包括兩端的UI、互動、業務和版本釋出時間不一致等差異。
前期探索
前期,我們嘗試通過一些設計方案來繞過上述差異,從而實現兩端的程式碼複用。我們選擇了二級頻道頁(下文統稱金剛頁)進行方案嘗試,設計如下:
其中,KingKongDelegate是Activity生命週期實現的代理類,包含onCreate、onResume等Activity生命週期回撥方法。在外賣App和外賣頻道兩端分別基於各自的基礎Activity實現WMKingKongAcitivity和MTKingKongActivity,分別會通過呼叫KingKongDelegate的方法對Activity的生命週期進行分發。KingKongInjector是兩端差異部分的介面集合,包括頁面跳轉(兩端頁面差異)、獲取頁面重新整理間隔時間、預設資源等,在外賣App和外賣頻道分別有對應的介面實現WMKingKongInjector和MTKingKongInjector。
NetworkController則是用Retrofit實現統一的網路請求封裝,PageListController是對列表分頁載入邏輯以及頁面空白、網路載入失敗等異常邏輯處理。
在金剛頁設計方案中,我們採用了“代理+繼承”的方式,實現了用統一的網路庫實現網路請求,定義了統一的基礎資料Model,統一了部分基礎服務以及基礎資料。通過KingKongDelegate遮蔽了兩端基礎Acitivity的差異,同時,通過KingKongInjector實現了兩端差異部分的處理。但是我們發現這種設計方案存在以下問題:
- 雖然這樣可以解決網路庫和圖片的差異,但是不能遮蔽兩端基礎Activity的差異。
- KingKongInjector提供了一種解決兩端差異的處理方式,但是KingKongInjector會存在很多不相關的方法集合,不易控制其邊界。此外,多個子模組需要呼叫KingKongInjector,會導致KingKongInjector不便管理。
- 由於兩端Model不同,需要實現這個模組使用的統一Model,但是並未和其他頁面使用的相同含義的Model統一。
平臺化複用方案設計
通過程式碼複用初步嘗試總結,我們總結出平臺化複用,需要考慮四件事情:
- 差異化的統一管理。
- 基礎服務的複用。
- 基礎元件的複用。
- 頁面的複用。
整體設計
我們在實現平臺化架構的基礎上,經過不斷的探索,最終形成適合外賣業務的平臺化複用設計:整體分為基礎服務層-基礎元件層-業務層-宿主層。設計圖如下:
- 基礎服務層:包含多端統一的基礎服務和有差異的基礎服務,其中統一的基礎服務包括網路庫、圖片庫、統計、監控等。對於登入、分享、定位等外賣App和外賣頻道兩端有差異的部分,我們通過抽象服務層來遮蔽兩端的差異。
- 基礎元件層:包括統一的兩端Model、埋點、下拉重新整理、許可權、Toast、A/B測試、Utils等兩端複用的基礎元件。
- 業務層:包括外賣的具體業務模組,目前可以分為列表頁模組(如首頁、金剛頁等)、商家模組(如商家頁、商品詳情頁等)和訂單模組(如下單頁、訂單狀態頁等)。這些業務模組的特點是:模組間複用可能性小,模組內的複用可能性大。
- 宿主層:主要是初始化服務,例如Application的初始化、dex載入和其他各種必要的元件的初始化。
分層架構能夠實現各層功能的職責分離,同時,我們要求上層不感知下層的多端差異。在各層中進行元件劃分,同樣,我們也要求實現呼叫元件方不感知元件的多端差異。通過這樣的設計,能夠使得整體架構更加清晰明朗,複用率提高的同時,不影響架構的複雜度和靈活度。
差異化管理
需要多端複用的業務相對於普通業務而言,最大的挑戰在於差異化管理。首先多端的先天條件就決定了多端複用業務會存在差異;其次,多端複用的業務有個性化的需求。在多端複用的差異化管理方案中,我們總結了以下兩種方案:
- 差異分支管理方案。
- pins工程+Flavor管理的方案。
差異分支管理
分支管理常用於多個需求在一端上線後,需要在另一端某一個時間節點跟進的場景,如下圖所示:
兩端開發1.0版本時,分別要在wm分支(外賣App對應分支)開發feature1和mt分支(外賣頻道對應分支)開發feature2。開發2.0版本時,feature1需要在外賣頻道上線,feature2需要在外賣App上線,則分別將feature1分支程式碼合入mt分支,feature2程式碼合入wm分支。這樣通過拉取新需求分支管理的方式,滿足了需求的差異化管理。但是這種實現方式存在兩個問題:- 兩端需求差異太多的話,就會存在很多分支,造成分支管理困難。
- 不支援細粒度的差異化管理,比如模組內部的差異化管理。
pins工程+Flavor的差異化管理
在Android官網《配置構建變體》章節中介紹了Product Flavor(下文簡稱Flavor)可以用於實現full版本以及demo版本的差異化管理,通過配置Gradle,可以基於不同的Flavor生成不同的apk版本。因此,模組內部的差異化管理是通過Flavor來實現,其原理如下圖所示:
其中Common是兩端複用的程式碼,DiffHandler是兩端差異部分介面,WMDiffHandler是外賣App對應的Flavor下的DiffHandler實現,MTDiffHandler是外賣頻道對應Flavor下的DiffHandler實現。通過兩端分別依賴不同Flavor程式碼實現模組內差異化管理。對於需求在兩端版本差異化管理,也可以通過配置Flavor來實現,如下圖所示:
在1.0版本時,feature1只在外賣App上線,feature2只在外賣頻道上線。當2.0版本時,如果feature1、feature2需要同時在兩端上線,只需要將對應業務程式碼移動到共用SourceSet即可實現feature1、feature2程式碼複用。綜合兩種差異程式碼實現來看,我們選擇使用Flavor方式來實現程式碼差異化管理。其優勢如下:
- 一個功能模組只需要維護一套程式碼。
- 差異程式碼在業務庫不同Flavor中實現,方便追溯程式碼實現歷史以及做差異實現對比。
- 對於上層來說,只會依賴下層程式碼的不同Flavor版本;下層對上層暴露介面也基本一樣,上層不用關心下層差異實現。
- 需求版本差異,也只需先在上線一端對應的Flavor中實現,當需要複用時移動到共用的SourceSet下面,就能實現需求程式碼複用。
從Android工程結構來看,使用Flavor只能在module內複用,但是以module為粒度的複用對於差異化管理來說約束太重。這意味著同個module內不同模組的差異程式碼同時存在於對應Flavor目錄下,或者說需要將每個子模組都建立成不同的module,這樣管理程式碼是非常不便的。《微信Android模組化架構重構實踐》一文中提到了一個重要的概念pins工程,pins工程能在module之內再次構建完整的多子工程結構。我們通過創造性的使用pins工程+Flavor的方案,將差異化的管理單元從module降到了pins工程。而pins工程可以定義到最小的業務單元,例如一個Java檔案。整體的設計實現如下:
具體的配置過程,首先需要在Android Studio工程裡首先要定義兩個Flavor:wm、mt。productFlavors {
wm {}
mt {}
}
複製程式碼
然後使用pins工程結構,把每個子業務作為一個pins工程,實現如下Gradle配置:
最終的工程目錄結構如下: 以名為base的pins工程為例,src/base/main是該工程的兩端共用程式碼,src/base/wm是該工程的外賣App使用的程式碼,src/base/mt是外賣頻道使用的程式碼。同時,我們做了程式碼檢查,除了base pins工程可以依賴以外,其他pins不存在直接依賴關係。通過這樣實現了module內部更細粒度的工程依賴,同時配合Gradle配置可以實現只編譯部分pins工程,使整體程式碼更加靈活。通過pins工程+Flavor的差異化管理方式,我們既實現了需求級別的差異化管理,也實現了模組內的功能差異化管理。同時,pins工程更好的控制了程式碼粒度以及程式碼邊界,也將差異程式碼控制在比module更小的粒度。
基礎服務的複用
對於一個App來說,基礎服務的重要性不言而喻,所以在平臺化複用中,往往基礎服務的差異最大。由於基礎服務的使用範圍比較廣,如果基礎服務的差異得不到有效的處理,讓上層感知到差異,就會增加架構層與層之間的耦合,上層本身實現業務的難度也會加大。下文裡講解一個我們在實踐過程中遇到的例子,來闡述我們的主要解決思路。
在前期探索章節中,我們提到金剛頁由於兩端基礎Activity差異,以致於要使用代理類來實現Activity生命週期分發。通過採用統一介面以及Flavor方式,我們可以統一兩端基礎Activity元件,如下圖所示:
分別將兩端WMBaseActivity和MTBaseActivity的差異介面統一成DialogController、ToastController以及ActionBarController等通用介面,然後在wm、mt兩個Flavor目錄下分別定義全限定名完全相同的BaseActivity,分別繼承MTBaseActivity和MTBaseActivity並實現統一介面,介面實現儘量保持一致。對於上層來說,如果繼承BaseActivity,其可呼叫的介面完全一致,從而達到遮蔽兩端基礎Activity差異的目的。對於一些通用基礎元件,由於使用範圍比較廣,如果不統一或者差異較大,會造成業務層程式碼實現差異較大,不利於程式碼複用。所以我們採用的策略是外賣App向外賣頻道看齊。程式碼複用前,外賣App主要使用的網路庫是Volley,統一切換為外賣頻道使用的MTRetrofit;外賣使用的圖片庫是Fresco,統一切換為外賣頻道使用的MTPicasso;其他統一的元件還包括動態載入框架、WebView載入元件、網路監控Cat、線上監控Holmes、日誌回撈Logan以及降級限流等。兩端程式碼複用時,修復問題、監控資料能力方面保持統一。
對於登入、定位等通用基礎服務,我們的原則是能統一儘量統一,這樣可以有效的減少多端複用中來帶的多端維護成本,多份變成一份。而對於無法統一的服務,抽象出統一的服務介面,讓上層不感知差異,從而減少上層的複用成本。
元件複用
元件化可以大大的提高一個App的複用率。對於平臺化複用的業務而言,也是一樣。多個模組之間也是會經常使用相同的功能,例如下拉重新整理、分頁載入、埋點、樣式等功能。將這些常用的功能抽離成元件供上層業務層呼叫,將可以大大提高複用效果。可以說元件化是平臺化複用的必要條件之一。
面對外賣App包含複雜眾多的業務功能,一個功能可以被拆分成元件的基本原則是不同業務庫中不同業務的共用的業務功能或行為功能。然後按照業務實現中相關性的遠近,自上而下的依賴性將抽離出來的元件劃分為基礎通用元件、基礎業務元件、UI公共元件。
基礎通用元件指那些變化不大,與業務無關的元件,例如頁面載入下拉重新整理元件(p_refresh),日誌記錄相關元件(p_log),異常兜底元件(p_exception)。基礎業務元件指以業務為基礎的元件:評論通用元件(p_ugc),埋點元件(p_judas),搜尋通用元件(p_search),紅包通用元件(p_coupon)等。UI公共元件指公用View或者UI樣式元件,與View 相關的通用元件(p_widget),與UI樣式相關的通用元件(p_theme)。
對於抽離出來的基礎元件,多端之間的差異怎麼處理呢? 例如兜底元件,外賣兜底樣式以黃色為主調,而外賣頻道中以綠色小團為主調,如圖所示:
我們首先將這個元件劃分為一個pins工程,對於多端的差異,在pins工程裡面利用Flavor管理多端之間的差異。這樣的方案,首先元件是一個獨立的模組,其次多端的差異在元件內部被統一處理了,上層業務不用感知元件的實現差異。而由於基礎服務層已經將差異化管理了,元件層也不用感知基礎服務的差異,減少了元件層的複用成本。頁面複用
對兩端同一個頁面來說,絕大部分的功能模組是可複用的,但是也存在不一致的功能模組。以外賣App和美團外賣頻道首頁為例,中部流量區等業務基本相同,但是頂部導航欄樣式功能和中部流量區佈局在兩端不一樣,如下圖所示:
針對上述問題,我們頁面複用的實現思路是頁面模組化:先將頁面功能按照業務相似性以及兩端差異拆分成高內聚低耦合的功能單元Block,然後兩端頁面使用拆分的功能單元Block像搭積木似的搭建頁面,單個的單元Block可以採用MVP模式實現。美團點評內部酒旅的Ripper和到店綜合Shield頁面模組化開發框架也是採用這樣的思路。由於我們要實現兩端複用,還要考慮頁面之間的差異。對於兩端頁面差異,我們統一使用上文中提到的Flavor機制在業務單元內對兩端差異化管理,業務單元所在頁面不感知業務單元的差異性。對於不同的差異,單元Block可以在MVP不同層做差異化管理。以首頁為例,首頁Block化複用架構如下圖。兩端首頁頭部導航欄UI展示、資料、功能不一樣,導航欄整個功能就以一個Flavor在兩端分別實現;商家列表中部流量區部分雖然整體UI佈局不一樣,但是裡面單個功能Block業務邏輯、整個資料一樣,繼續將中部流量區裡面的業務Block化;下方的商家列表項兩端一樣的功能,用一個公有的Block實現。在各個單元Block已經實現的基礎上,兩端首頁搭建成首頁Fragment。
頁面模組化後,將兩端不同的差異在各個單元Block以Flavor方式處理,業務單元Block所在頁面不用關心各個Block實現差異,不僅實現了頁面的複用,各個模組功能職責分離,還提高了可維護性。總結和展望
美團外賣業務需要在外賣平臺和美團平臺同時部署,因此,在美團外賣平臺化架構過程中就產生了平臺化複用的問題。而怎麼去實現平臺化複用呢?筆者認為需要從不同粒度去考慮:基礎服務、元件、頁面。對於基礎服務,我們需要儘可能的統一,不能統一的就抽象服務層。元件級別,需要分塊分層,將依賴梳理好。頁面的複用,最重要的是頁面模組化和頁面內模組做到職責分離。平臺化複用最大的難點在於:差異的管理和遮蔽。本文提出使用pins工程+Flavor的方案,可以使得差異程式碼的管理得到有效的解決。同時利用分層策略,每層都自己處理好自己的差異,使得上層不用關心下層的差異。平臺化複用不能單純的追求複用率,同時要考慮到端的個性化。
到目前為止,我們實現了絕大部分外賣App和外賣頻道程式碼複用,整體程式碼複用率達到88.35%,人效提升70%以上。未來,我們可能會在外賣平臺、美團平臺、大眾點評平臺三個平臺進行程式碼複用,其場景將會更加複雜。當然,我們在做平臺化複用的時候,要合理地進行評估,複用帶來的“成本節約”和為了複用帶來的“成本增加”之間的比率。另外,平臺化複用視角不應該侷限於業務頁面的複用,對於監控、測試、研發工具、運維工具等也可以進行復用,這也是平臺化複用理念的核心價值所在。
參考資料
作者簡介
曉飛,美團點評技術專家。2015年加入美團點評,外賣Android的早期開發者之一。目前是外賣Android App負責人,主要負責版本管理和業務架構。
金光,美團點評高階工程師。2017年加入美團點評,主要負責程式碼複用及外賣平臺化相關工作。
王芳,美團點評高階工程師。2017年加入美團點評,主要負責商家列表頁面等相關頁面業務。
招聘
美團外賣長期招聘Android、iOS、FE 高階/資深工程師和技術專家,Base 北京、上海、成都,歡迎有興趣的同學投遞簡歷到wukai05@meituan.com。