美團外賣平台化復用主要是指多端代碼復用,正如美團外賣iOS多端復用的推動、支撐與思考文章所述,多端包含有兩層意思:其一是相同業務的多入口,指美團外賣業務需要在美團外賣App(下文簡稱外賣App)和美團App外賣頻道(下文簡稱外賣頻道)同時上線;其二是指平台上各個業務線,美團外賣不同業務線都依賴外賣基礎服務,比如登陸、定位等。多入口及多業務線給美團外賣平台化復用帶來了巨大的挑戰,本文將在美團外賣Android平台化架構演進實踐文中“代碼復用”章節的基礎上,進一步詳細地介紹平台化復用工作面臨的挑戰以及相應的解決方案。
美團外賣平台化復用背景
美團外賣App和美團App外賣頻道業務基本一樣,但由於歷史原因,兩端代碼差異較大,造成同樣的子業務需求在一端上線后,另一端幾乎需要重新實現,嚴重浪費開發資源。在美團外賣Android平台化架構演進實踐文章中,將美團外賣Android客戶端平台架構分為平台層、業務層和宿主層,我們希望能夠在平台化架構中實現平台層和業務層的多端復用,節省子業務需求實現多端部署的開發資源。本文主要講述在美團外賣平台化過程中多端代碼復用的實踐方案,多端主要指的外賣App和外賣頻道這兩端。
難點總結
兩端業務雖然基本一致,但是仍舊存在差異,UI、基礎服務、需求差異等。這些差異存在於平台化架構中的平台層和業務層各個模塊中,給平台化復用帶來了巨大的挑戰。我們總結了兩端代碼的差異點,主要包括以下幾個方面:
- 基礎服務的差異:包括基礎Activity、網絡庫、圖片庫等底層庫的差異。
- 組件的實現差異:包括基礎數據Model、下拉刷新、頁面跳轉等基礎組件的差異。
- 頁面的差異:包括怎么處理兩端的UI、交互、業務和版本發版時間不一致的差異。
前期探索
前期,我們嘗試通過一些設計的方案來繞過上述的差異,從而實現兩端的代碼復用。我們選擇了二級頻道頁(以下統稱金剛頁)進行方案嘗試,設計如下:
KingKongDeletegate是Activity生命周期實現的代理類,包含onCreate、onResume等Activity生命周期回調方法。在外賣App和外賣頻道兩端分別基於各自的基礎Activity實現WMKingKongAcitivity和MTKingKongActivity,分別會通過調用KingKongDeletegate的方法對Activity的生命周期進行分發。
KingKongInjector是兩端差異部分的接口集合,包括頁面跳轉(兩端頁面差異)、獲取頁面刷新間隔時間、默認資源等,在外賣App和外賣頻道分別有對應的接口實現WMKingkongInjector和MTKingkongInjector。
NetworkController則是用Retrofit實現統一的網絡請求封裝,PageListController是對列表分頁加載邏輯以及頁面空白、網絡加載失敗等異常邏輯處理。
在金剛頁設計方案中,我們采用了“代理+繼承”的方式,實現了用統一的網絡庫實現網絡請求,定義了統一的基礎數據Model,統一了部分基礎服務以及基礎數據。通過KingKongDelegate屏蔽了兩端基礎Acitivity的差異,同時,通過KingKongInjector實現了兩端差異部分的處理。但是我們發現這種設計方案存在以下問題:
- 雖然這樣可以解決網絡庫和圖片的差異,但是不能屏蔽兩端基礎Activity的差異。
- KingKongInjector提供了一種解決兩端差異的處理方式,但是KingKongInjector會存在很多不相關的方法集合,不易控制其邊界。此外,多個子模塊需要調用KinKongInjector,會導致KingKongInjector不便管理。
- 由於兩端Model不同,需要實現這個模塊使用的統一Model,並未和其他頁面使用的相同含義的Model統一。
平台化復用方案設計
通過代碼復用初步嘗試總結,我們總結出平台化復用,需要考慮四件事情:
- 差異化的統一管理。
- 基礎服務的復用。
- 基礎組件的復用。
- 頁面的復用。
整體設計
在美團外賣Android平台化架構演進實踐實現平台化架構的基礎上,經過不斷的探索,最終形成適合外賣業務的平台化復用設計:整體分為基礎服務層-基礎組件層-業務層-宿主層。設計圖如下:
- 基礎服務層:包含多端統一的基礎服務和有差異的基礎服務,其中統一的基礎服務包括網絡庫、圖片庫、統計、監控等。對於登陸、分享、定位等外賣App和外賣頻道兩端有差異的部分,我們通過抽象服務層來屏蔽兩端的差異。
- 基礎組件層:包括統一的兩端Model、埋點、下拉、刷新、權限、Toast、A/B測試、Utils等兩端復用的基礎組件。
- 業務層:包括外賣的具體業務模塊,目前可以分為列表頁模塊(如首頁、金剛頁等)、點商品模塊(如商家頁、商品詳情頁等)和訂單模塊(如下單頁、訂單狀態頁等)。這些業務模塊的特點是:模塊間復用可能性小,模塊內的復用可能性大。
- 宿主層:主要是初始化服務,例如Application的初始化、dex加載和其他各種必要的組件的初始化。
分層架構能夠實現各層功能的職責分離,同時,我們要求上層不感知下層的多端差異。在各層中進行組件划分,同樣,我們也要求實現調用組件方不感知組件的多端差異。通過這樣的設計,能夠使得整體架構更加清晰明朗,復用率提高的同時,不影響架構的復雜度和靈活度。
差異化管理
需要多端復用的業務相對於普通業務而言,最大的挑戰在於差異化管理。首先多端的先天條件就決定了多端復用業務會存在差異;其次,多端復用的業務有個性化的需求。在多端復用的差異化管理方案中,我們總結了以下2種方案:
- 差異分支管理方案。
- 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。
發現文章有錯誤、對內容有疑問,都可以關注美團點評技術團隊微信公眾號(meituantech),在后台給我們留言。我們每周會挑選出一位熱心小伙伴,送上一份精美的小禮品。快來掃碼關注我們吧!