美团外卖Android平台化的复用实践
美團(tuán)外賣平臺(tái)化復(fù)用主要是指多端代碼復(fù)用,正如美團(tuán)外賣iOS多端復(fù)用的推動(dòng)、支撐與思考文章所述,多端包含有兩層意思:其一是相同業(yè)務(wù)的多入口,指美團(tuán)外賣業(yè)務(wù)需要在美團(tuán)外賣App(下文簡稱外賣App)和美團(tuán)App外賣頻道(下文簡稱外賣頻道)同時(shí)上線;其二是指平臺(tái)上各個(gè)業(yè)務(wù)線,美團(tuán)外賣不同業(yè)務(wù)線都依賴外賣基礎(chǔ)服務(wù),比如登陸、定位等。
多入口及多業(yè)務(wù)線給美團(tuán)外賣平臺(tái)化復(fù)用帶來了巨大的挑戰(zhàn),此前我們的一篇博客《美團(tuán)外賣Android平臺(tái)化架構(gòu)演進(jìn)實(shí)踐》(下文簡稱《架構(gòu)演進(jìn)實(shí)踐》)也提到了這個(gè)問題,本文將在“代碼復(fù)用”這一章節(jié)的基礎(chǔ)上,進(jìn)一步介紹平臺(tái)化復(fù)用工作面臨的挑戰(zhàn)以及相應(yīng)的解決方案。
美團(tuán)外賣平臺(tái)化復(fù)用背景
美團(tuán)外賣App和美團(tuán)App外賣頻道業(yè)務(wù)基本一樣,但由于歷史原因,兩端代碼差異較大,造成同樣的子業(yè)務(wù)需求在一端上線后,另一端幾乎需要重新實(shí)現(xiàn),嚴(yán)重浪費(fèi)開發(fā)資源。在《架構(gòu)演進(jìn)實(shí)踐》一文中,將美團(tuán)外賣Android客戶端平臺(tái)化架構(gòu)分為平臺(tái)層、業(yè)務(wù)層和宿主層,我們希望能夠在平臺(tái)化架構(gòu)中實(shí)現(xiàn)平臺(tái)層和業(yè)務(wù)層的多端復(fù)用,從而節(jié)省子業(yè)務(wù)需求開發(fā)資源,實(shí)現(xiàn)多端部署。
難點(diǎn)總結(jié)
兩端業(yè)務(wù)雖然基本一致,但是仍舊存在差異,UI、基礎(chǔ)服務(wù)、需求差異等。這些差異存在于美團(tuán)外賣平臺(tái)化架構(gòu)中的平臺(tái)層和業(yè)務(wù)層各個(gè)模塊中,給平臺(tái)化復(fù)用帶來了巨大的挑戰(zhàn)。我們總結(jié)了兩端代碼的差異點(diǎn),主要包括以下幾個(gè)方面:
前期探索
前期,我們嘗試通過一些設(shè)計(jì)方案來繞過上述差異,從而實(shí)現(xiàn)兩端的代碼復(fù)用。我們選擇了二級(jí)頻道頁(下文統(tǒng)稱金剛頁)進(jìn)行方案嘗試,設(shè)計(jì)如下:
其中,KingKongDelegate是Activity生命周期實(shí)現(xiàn)的代理類,包含onCreate、onResume等Activity生命周期回調(diào)方法。在外賣App和外賣頻道兩端分別基于各自的基礎(chǔ)Activity實(shí)現(xiàn)WMKingKongAcitivity和MTKingKongActivity,分別會(huì)通過調(diào)用KingKongDelegate的方法對(duì)Activity的生命周期進(jìn)行分發(fā)。
KingKongInjector是兩端差異部分的接口集合,包括頁面跳轉(zhuǎn)(兩端頁面差異)、獲取頁面刷新間隔時(shí)間、默認(rèn)資源等,在外賣App和外賣頻道分別有對(duì)應(yīng)的接口實(shí)現(xiàn)WMKingKongInjector和MTKingKongInjector。
NetworkController則是用Retrofit實(shí)現(xiàn)統(tǒng)一的網(wǎng)絡(luò)請(qǐng)求封裝,PageListController是對(duì)列表分頁加載邏輯以及頁面空白、網(wǎng)絡(luò)加載失敗等異常邏輯處理。
在金剛頁設(shè)計(jì)方案中,我們采用了“代理+繼承”的方式,實(shí)現(xiàn)了用統(tǒng)一的網(wǎng)絡(luò)庫實(shí)現(xiàn)網(wǎng)絡(luò)請(qǐng)求,定義了統(tǒng)一的基礎(chǔ)數(shù)據(jù)Model,統(tǒng)一了部分基礎(chǔ)服務(wù)以及基礎(chǔ)數(shù)據(jù)。通過KingKongDelegate屏蔽了兩端基礎(chǔ)Acitivity的差異,同時(shí),通過KingKongInjector實(shí)現(xiàn)了兩端差異部分的處理。但是我們發(fā)現(xiàn)這種設(shè)計(jì)方案存在以下問題:
平臺(tái)化復(fù)用方案設(shè)計(jì)
通過代碼復(fù)用初步嘗試總結(jié),我們總結(jié)出平臺(tái)化復(fù)用,需要考慮四件事情:
整體設(shè)計(jì)
我們?cè)趯?shí)現(xiàn)平臺(tái)化架構(gòu)的基礎(chǔ)上,經(jīng)過不斷的探索,最終形成適合外賣業(yè)務(wù)的平臺(tái)化復(fù)用設(shè)計(jì):整體分為基礎(chǔ)服務(wù)層-基礎(chǔ)組件層-業(yè)務(wù)層-宿主層。設(shè)計(jì)圖如下:
分層架構(gòu)能夠?qū)崿F(xiàn)各層功能的職責(zé)分離,同時(shí),我們要求上層不感知下層的多端差異。在各層中進(jìn)行組件劃分,同樣,我們也要求實(shí)現(xiàn)調(diào)用組件方不感知組件的多端差異。通過這樣的設(shè)計(jì),能夠使得整體架構(gòu)更加清晰明朗,復(fù)用率提高的同時(shí),不影響架構(gòu)的復(fù)雜度和靈活度。
差異化管理
需要多端復(fù)用的業(yè)務(wù)相對(duì)于普通業(yè)務(wù)而言,最大的挑戰(zhàn)在于差異化管理。首先多端的先天條件就決定了多端復(fù)用業(yè)務(wù)會(huì)存在差異;其次,多端復(fù)用的業(yè)務(wù)有個(gè)性化的需求。在多端復(fù)用的差異化管理方案中,我們總結(jié)了以下兩種方案:
差異分支管理
分支管理常用于多個(gè)需求在一端上線后,需要在另一端某一個(gè)時(shí)間節(jié)點(diǎn)跟進(jìn)的場景,如下圖所示:
兩端開發(fā)1.0版本時(shí),分別要在wm分支(外賣App對(duì)應(yīng)分支)開發(fā)feature1和mt分支(外賣頻道對(duì)應(yīng)分支)開發(fā)feature2。開發(fā)2.0版本時(shí),feature1需要在外賣頻道上線,feature2需要在外賣App上線,則分別將feature1分支代碼合入mt分支,feature2代碼合入wm分支。這樣通過拉取新需求分支管理的方式,滿足了需求的差異化管理。但是這種實(shí)現(xiàn)方式存在兩個(gè)問題:
pins工程+Flavor的差異化管理
在Android官網(wǎng)《配置構(gòu)建變體》章節(jié)中介紹了Product Flavor(下文簡稱Flavor)可以用于實(shí)現(xiàn)full版本以及demo版本的差異化管理,通過配置Gradle,可以基于不同的Flavor生成不同的apk版本。因此,模塊內(nèi)部的差異化管理是通過Flavor來實(shí)現(xiàn),其原理如下圖所示:
其中Common是兩端復(fù)用的代碼,DiffHandler是兩端差異部分接口,WMDiffHandler是外賣App對(duì)應(yīng)的Flavor下的DiffHandler實(shí)現(xiàn),MTDiffHandler是外賣頻道對(duì)應(yīng)Flavor下的DiffHandler實(shí)現(xiàn)。通過兩端分別依賴不同F(xiàn)lavor代碼實(shí)現(xiàn)模塊內(nèi)差異化管理。
對(duì)于需求在兩端版本差異化管理,也可以通過配置Flavor來實(shí)現(xiàn),如下圖所示:
在1.0版本時(shí),feature1只在外賣App上線,feature2只在外賣頻道上線。當(dāng)2.0版本時(shí),如果feature1、feature2需要同時(shí)在兩端上線,只需要將對(duì)應(yīng)業(yè)務(wù)代碼移動(dòng)到共用SourceSet即可實(shí)現(xiàn)feature1、feature2代碼復(fù)用。
綜合兩種差異代碼實(shí)現(xiàn)來看,我們選擇使用Flavor方式來實(shí)現(xiàn)代碼差異化管理。其優(yōu)勢如下:
從Android工程結(jié)構(gòu)來看,使用Flavor只能在module內(nèi)復(fù)用,但是以module為粒度的復(fù)用對(duì)于差異化管理來說約束太重。這意味著同個(gè)module內(nèi)不同模塊的差異代碼同時(shí)存在于對(duì)應(yīng)Flavor目錄下,或者說需要將每個(gè)子模塊都創(chuàng)建成不同的module,這樣管理代碼是非常不便的。《微信Android模塊化架構(gòu)重構(gòu)實(shí)踐》一文中提到了一個(gè)重要的概念pins工程,pins工程能在module之內(nèi)再次構(gòu)建完整的多子工程結(jié)構(gòu)。我們通過創(chuàng)造性的使用pins工程+Flavor的方案,將差異化的管理單元從module降到了pins工程。而pins工程可以定義到最小的業(yè)務(wù)單元,例如一個(gè)Java文件。整體的設(shè)計(jì)實(shí)現(xiàn)如下:
具體的配置過程,首先需要在Android Studio工程里首先要定義兩個(gè)Flavor:wm、mt。
productFlavors {wm {}mt {} }然后使用pins工程結(jié)構(gòu),把每個(gè)子業(yè)務(wù)作為一個(gè)pins工程,實(shí)現(xiàn)如下Gradle配置:
最終的工程目錄結(jié)構(gòu)如下:
以名為base的pins工程為例,src/base/main是該工程的兩端共用代碼,src/base/wm是該工程的外賣App使用的代碼,src/base/mt是外賣頻道使用的代碼。同時(shí),我們做了代碼檢查,除了base pins工程可以依賴以外,其他pins不存在直接依賴關(guān)系。通過這樣實(shí)現(xiàn)了module內(nèi)部更細(xì)粒度的工程依賴,同時(shí)配合Gradle配置可以實(shí)現(xiàn)只編譯部分pins工程,使整體代碼更加靈活。
通過pins工程+Flavor的差異化管理方式,我們既實(shí)現(xiàn)了需求級(jí)別的差異化管理,也實(shí)現(xiàn)了模塊內(nèi)的功能差異化管理。同時(shí),pins工程更好的控制了代碼粒度以及代碼邊界,也將差異代碼控制在比module更小的粒度。
基礎(chǔ)服務(wù)的復(fù)用
對(duì)于一個(gè)App來說,基礎(chǔ)服務(wù)的重要性不言而喻,所以在平臺(tái)化復(fù)用中,往往基礎(chǔ)服務(wù)的差異最大。由于基礎(chǔ)服務(wù)的使用范圍比較廣,如果基礎(chǔ)服務(wù)的差異得不到有效的處理,讓上層感知到差異,就會(huì)增加架構(gòu)層與層之間的耦合,上層本身實(shí)現(xiàn)業(yè)務(wù)的難度也會(huì)加大。下文里講解一個(gè)我們?cè)趯?shí)踐過程中遇到的例子,來闡述我們的主要解決思路。
在前期探索章節(jié)中,我們提到金剛頁由于兩端基礎(chǔ)Activity差異,以致于要使用代理類來實(shí)現(xiàn)Activity生命周期分發(fā)。通過采用統(tǒng)一接口以及Flavor方式,我們可以統(tǒng)一兩端基礎(chǔ)Activity組件,如下圖所示:
分別將兩端WMBaseActivity和MTBaseActivity的差異接口統(tǒng)一成DialogController、ToastController以及ActionBarController等通用接口,然后在wm、mt兩個(gè)Flavor目錄下分別定義全限定名完全相同的BaseActivity,分別繼承MTBaseActivity和MTBaseActivity并實(shí)現(xiàn)統(tǒng)一接口,接口實(shí)現(xiàn)盡量保持一致。對(duì)于上層來說,如果繼承BaseActivity,其可調(diào)用的接口完全一致,從而達(dá)到屏蔽兩端基礎(chǔ)Activity差異的目的。
對(duì)于一些通用基礎(chǔ)組件,由于使用范圍比較廣,如果不統(tǒng)一或者差異較大,會(huì)造成業(yè)務(wù)層代碼實(shí)現(xiàn)差異較大,不利于代碼復(fù)用。所以我們采用的策略是外賣App向外賣頻道看齊。代碼復(fù)用前,外賣App主要使用的網(wǎng)絡(luò)庫是Volley,統(tǒng)一切換為外賣頻道使用的MTRetrofit;外賣使用的圖片庫是Fresco,統(tǒng)一切換為外賣頻道使用的MTPicasso;其他統(tǒng)一的組件還包括動(dòng)態(tài)加載框架、WebView加載組件、網(wǎng)絡(luò)監(jiān)控Cat、線上監(jiān)控Holmes、日志回?fù)芁ogan以及降級(jí)限流等。兩端代碼復(fù)用時(shí),修復(fù)問題、監(jiān)控?cái)?shù)據(jù)能力方面保持統(tǒng)一。
對(duì)于登錄、定位等通用基礎(chǔ)服務(wù),我們的原則是能統(tǒng)一盡量統(tǒng)一,這樣可以有效的減少多端復(fù)用中來帶的多端維護(hù)成本,多份變成一份。而對(duì)于無法統(tǒng)一的服務(wù),抽象出統(tǒng)一的服務(wù)接口,讓上層不感知差異,從而減少上層的復(fù)用成本。
組件復(fù)用
組件化可以大大的提高一個(gè)App的復(fù)用率。對(duì)于平臺(tái)化復(fù)用的業(yè)務(wù)而言,也是一樣。多個(gè)模塊之間也是會(huì)經(jīng)常使用相同的功能,例如下拉刷新、分頁加載、埋點(diǎn)、樣式等功能。將這些常用的功能抽離成組件供上層業(yè)務(wù)層調(diào)用,將可以大大提高復(fù)用效果。可以說組件化是平臺(tái)化復(fù)用的必要條件之一。
面對(duì)外賣App包含復(fù)雜眾多的業(yè)務(wù)功能,一個(gè)功能可以被拆分成組件的基本原則是不同業(yè)務(wù)庫中不同業(yè)務(wù)的共用的業(yè)務(wù)功能或行為功能。然后按照業(yè)務(wù)實(shí)現(xiàn)中相關(guān)性的遠(yuǎn)近,自上而下的依賴性將抽離出來的組件劃分為基礎(chǔ)通用組件、基礎(chǔ)業(yè)務(wù)組件、UI公共組件。
基礎(chǔ)通用組件指那些變化不大,與業(yè)務(wù)無關(guān)的組件,例如頁面加載下拉刷新組件(p_refresh),日志記錄相關(guān)組件(p_log),異常兜底組件(p_exception)。基礎(chǔ)業(yè)務(wù)組件指以業(yè)務(wù)為基礎(chǔ)的組件:評(píng)論通用組件(p_ugc),埋點(diǎn)組件(p_judas),搜索通用組件(p_search),紅包通用組件(p_coupon)等。UI公共組件指公用View或者UI樣式組件,與View 相關(guān)的通用組件(p_widget),與UI樣式相關(guān)的通用組件(p_theme)。
對(duì)于抽離出來的基礎(chǔ)組件,多端之間的差異怎么處理呢? 例如兜底組件,外賣兜底樣式以黃色為主調(diào),而外賣頻道中以綠色小團(tuán)為主調(diào),如圖所示:
我們首先將這個(gè)組件劃分為一個(gè)pins工程,對(duì)于多端的差異,在pins工程里面利用Flavor管理多端之間的差異。這樣的方案,首先組件是一個(gè)獨(dú)立的模塊,其次多端的差異在組件內(nèi)部被統(tǒng)一處理了,上層業(yè)務(wù)不用感知組件的實(shí)現(xiàn)差異。而由于基礎(chǔ)服務(wù)層已經(jīng)將差異化管理了,組件層也不用感知基礎(chǔ)服務(wù)的差異,減少了組件層的復(fù)用成本。
頁面復(fù)用
對(duì)兩端同一個(gè)頁面來說,絕大部分的功能模塊是可復(fù)用的,但是也存在不一致的功能模塊。以外賣App和美團(tuán)外賣頻道首頁為例,中部流量區(qū)等業(yè)務(wù)基本相同,但是頂部導(dǎo)航欄樣式功能和中部流量區(qū)布局在兩端不一樣,如下圖所示:
針對(duì)上述問題,我們頁面復(fù)用的實(shí)現(xiàn)思路是頁面模塊化:先將頁面功能按照業(yè)務(wù)相似性以及兩端差異拆分成高內(nèi)聚低耦合的功能單元Block,然后兩端頁面使用拆分的功能單元Block像搭積木似的搭建頁面,單個(gè)的單元Block可以采用MVP模式實(shí)現(xiàn)。美團(tuán)點(diǎn)評(píng)內(nèi)部酒旅的Ripper和到店綜合Shield頁面模塊化開發(fā)框架也是采用這樣的思路。由于我們要實(shí)現(xiàn)兩端復(fù)用,還要考慮頁面之間的差異。對(duì)于兩端頁面差異,我們統(tǒng)一使用上文中提到的Flavor機(jī)制在業(yè)務(wù)單元內(nèi)對(duì)兩端差異化管理,業(yè)務(wù)單元所在頁面不感知業(yè)務(wù)單元的差異性。對(duì)于不同的差異,單元Block可以在MVP不同層做差異化管理。
以首頁為例,首頁Block化復(fù)用架構(gòu)如下圖。兩端首頁頭部導(dǎo)航欄UI展示、數(shù)據(jù)、功能不一樣,導(dǎo)航欄整個(gè)功能就以一個(gè)Flavor在兩端分別實(shí)現(xiàn);商家列表中部流量區(qū)部分雖然整體UI布局不一樣,但是里面單個(gè)功能Block業(yè)務(wù)邏輯、整個(gè)數(shù)據(jù)一樣,繼續(xù)將中部流量區(qū)里面的業(yè)務(wù)Block化;下方的商家列表項(xiàng)兩端一樣的功能,用一個(gè)公有的Block實(shí)現(xiàn)。在各個(gè)單元Block已經(jīng)實(shí)現(xiàn)的基礎(chǔ)上,兩端首頁搭建成首頁Fragment。
頁面模塊化后,將兩端不同的差異在各個(gè)單元Block以Flavor方式處理,業(yè)務(wù)單元Block所在頁面不用關(guān)心各個(gè)Block實(shí)現(xiàn)差異,不僅實(shí)現(xiàn)了頁面的復(fù)用,各個(gè)模塊功能職責(zé)分離,還提高了可維護(hù)性。
總結(jié)
美團(tuán)外賣業(yè)務(wù)需要在外賣平臺(tái)和美團(tuán)平臺(tái)同時(shí)部署,因此,在美團(tuán)外賣平臺(tái)化架構(gòu)過程中就產(chǎn)生了平臺(tái)化復(fù)用的問題。而怎么去實(shí)現(xiàn)平臺(tái)化復(fù)用呢?筆者認(rèn)為需要從不同粒度去考慮:基礎(chǔ)服務(wù)、組件、頁面。對(duì)于基礎(chǔ)服務(wù),我們需要盡可能的統(tǒng)一,不能統(tǒng)一的就抽象服務(wù)層。組件級(jí)別,需要分塊分層,將依賴梳理好。頁面的復(fù)用,最重要的是頁面模塊化和頁面內(nèi)模塊做到職責(zé)分離。平臺(tái)化復(fù)用最大的難點(diǎn)在于:差異的管理和屏蔽。本文提出使用pins工程+Flavor的方案,可以使得差異代碼的管理得到有效的解決。同時(shí)利用分層策略,每層都自己處理好自己的差異,使得上層不用關(guān)心下層的差異。平臺(tái)化復(fù)用不能單純的追求復(fù)用率,同時(shí)要考慮到端的個(gè)性化。
到目前為止,我們實(shí)現(xiàn)了絕大部分外賣App和外賣頻道代碼復(fù)用,整體代碼復(fù)用率達(dá)到88.35%,人效提升70%以上。未來,我們可能會(huì)在外賣平臺(tái)、美團(tuán)平臺(tái)、大眾點(diǎn)評(píng)平臺(tái)三個(gè)平臺(tái)進(jìn)行代碼復(fù)用,其場景將會(huì)更加復(fù)雜。當(dāng)然,我們?cè)谧銎脚_(tái)化復(fù)用的時(shí)候,要合理地進(jìn)行評(píng)估,復(fù)用帶來的“成本節(jié)約”和為了復(fù)用帶來的“成本增加”之間的比率。另外,平臺(tái)化復(fù)用視角不應(yīng)該局限于業(yè)務(wù)頁面的復(fù)用,對(duì)于監(jiān)控、測試、研發(fā)工具、運(yùn)維工具等也可以進(jìn)行復(fù)用,這也是平臺(tái)化復(fù)用理念的核心價(jià)值所在。
參考資料
作者簡介
- 曉飛,美團(tuán)點(diǎn)評(píng)技術(shù)專家。2015年加入美團(tuán)點(diǎn)評(píng),外賣Android的早期開發(fā)者之一。目前是外賣Android App負(fù)責(zé)人,主要負(fù)責(zé)版本管理和業(yè)務(wù)架構(gòu)。
- 金光,美團(tuán)點(diǎn)評(píng)高級(jí)工程師。2017年加入美團(tuán)點(diǎn)評(píng),主要負(fù)責(zé)代碼復(fù)用及外賣平臺(tái)化相關(guān)工作。
- 王芳,美團(tuán)點(diǎn)評(píng)高級(jí)工程師。2017年加入美團(tuán)點(diǎn)評(píng),主要負(fù)責(zé)商家列表頁面等相關(guān)頁面業(yè)務(wù)。
招聘
美團(tuán)外賣長期招聘Android、iOS、FE 高級(jí)/資深工程師和技術(shù)專家,Base 北京、上海、成都,歡迎有興趣的同學(xué)投遞簡歷到wukai05@meituan.com。
總結(jié)
以上是生活随笔為你收集整理的美团外卖Android平台化的复用实践的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 消费者驱动的微服务契约测试套件Sprin
- 下一篇: Android 兼容 Java 8 语法