美团外卖iOS多端复用的推动、支撑与思考
前言
美團外賣2013年11月開始起步,隨后高速發(fā)展,不斷刷新多項行業(yè)記錄。截止至2018年5月19日,日訂單量峰值已超過2000萬,是全球規(guī)模最大的外賣平臺。業(yè)務(wù)的快速發(fā)展對技術(shù)支撐提出了更高的要求。為線上用戶提供高穩(wěn)定的服務(wù)體驗,保障全鏈路業(yè)務(wù)和系統(tǒng)高可用運行的同時,要提升多入口業(yè)務(wù)的研發(fā)速度,推進App系統(tǒng)架構(gòu)的合理演化,進一步提升跨部門跨地域團隊之間的協(xié)作效率。而另一方面隨著用戶數(shù)與訂單數(shù)的高速增長,美團外賣逐漸有了流量平臺的特征,兄弟業(yè)務(wù)紛紛嘗試接入美團外賣進行推廣和發(fā)布,期望提供統(tǒng)一標準化服務(wù)平臺。因此,基礎(chǔ)能力標準化,推進多端復(fù)用,同時輸出成熟穩(wěn)定的技術(shù)服務(wù)平臺,一直是我們技術(shù)團隊追求的核心目標。
多端復(fù)用的端
這里的“端”有兩層意思:
其一是相同業(yè)務(wù)的多入口
美團外賣在iOS下的業(yè)務(wù)入口有三個,『美團外賣』App、『美團』App的外賣頻道、『大眾點評』App的外賣頻道。
值得一提的是:由于用戶畫像與產(chǎn)品策略差異,『大眾點評』外賣頻道與『美團』外賣頻道和『美團外賣』雖經(jīng)歷技術(shù)棧融合,但業(yè)務(wù)形態(tài)區(qū)別較大,暫不考慮上層業(yè)務(wù)的復(fù)用,故這篇文章主要介紹美團系兩大入口的復(fù)用。
在2015年外賣C端合并之前,美團系的兩大入口由兩個不同的團隊研發(fā),雖然用戶感知的交互界面幾乎相同,但功能實現(xiàn)層面的代碼風格和技術(shù)棧都存在較大差異,同一需求需要在兩端重復(fù)開發(fā)顯然不合理。所以,我們的目標是相同功能,只需要寫一次代碼,做一次估時,其他端只需做少量的適配工作。
其二是指平臺上各個業(yè)務(wù)線
外賣不同兄弟業(yè)務(wù)線都依賴外賣基礎(chǔ)業(yè)務(wù),包括但不限于:地圖定位、登錄綁定、網(wǎng)絡(luò)通道、異常處理、工具UI等。考慮到標準化的范疇,這些基礎(chǔ)能力也是需要多端復(fù)用的。
關(guān)于組件化
提到多端復(fù)用,不免與組件化產(chǎn)生聯(lián)系,可以說組件化是多端復(fù)用的必要條件之一。大多數(shù)公司口中的“組件化”僅僅做到代碼分庫,使用Cocoapods的Podfile來管理,再在主工程把各個子庫的版本號聚合起來。但是能設(shè)計一套合理的分層架構(gòu),理清依賴關(guān)系,并有一整套工具鏈支撐組件發(fā)版與集成的相對較少。否則組件化只會導(dǎo)致包體積增大,開發(fā)效率變慢,依賴關(guān)系復(fù)雜等副作用。
整體思路
A. 多端復(fù)用概念圖
多端復(fù)用的目標形態(tài)其實很好理解,就是將原有主工程中的代碼抽出獨立組件(Pods),然后各自工程使用Podfile依賴所需的獨立組件,獨立組件再通過podspec間接依賴其他獨立組件。
B. 準備工作
確認多端所依賴的基層庫是一致的,這里的基層庫包括開源庫與公司內(nèi)的技術(shù)棧。
iOS中常用開源庫(網(wǎng)絡(luò)、圖片、布局)每個功能基本都有一個庫業(yè)界壟斷,這一點是iOS相對于Android的優(yōu)勢。公司內(nèi)也存在一些對開源庫二次開發(fā)或自行研發(fā)的基礎(chǔ)庫,即技術(shù)棧。不同的大組之間技術(shù)棧可能存在一定差異。如需要復(fù)用的端之間存在差異,則需要重構(gòu)使得技術(shù)棧統(tǒng)一。(這里建議重構(gòu),不建議適配,因為如果做的不夠徹底,后續(xù)很大可能需要填坑。)
就美團而言,美團平臺與點評平臺作為公司兩大App,歷史積淀厚重。自2015年底合并以來,為了共建和沉淀公共服務(wù),減少重復(fù)造輪子,提升研發(fā)效率,對上層業(yè)務(wù)方提供統(tǒng)一標準的高穩(wěn)定基礎(chǔ)能力,兩大平臺的底層技術(shù)棧也在不斷融合。而美團外賣作為較早實踐獨立App,同時也是依托于兩大平臺App的大業(yè)務(wù)方,在外賣C端合并后的1年內(nèi),我們也做了大量底層技術(shù)棧統(tǒng)一的必要工作。
C. 方案選型
在演進式設(shè)計與計劃式設(shè)計中的抉擇。
演進式設(shè)計指隨著系統(tǒng)的開發(fā)而做設(shè)計變更,而計劃式設(shè)計是指在開發(fā)之前完全指定系統(tǒng)架構(gòu)的設(shè)計。演進的設(shè)計,同樣需要遵循架構(gòu)設(shè)計的基本準則,它與計劃的設(shè)計唯一的區(qū)別是設(shè)計的目標。演進的設(shè)計提倡滿足客戶現(xiàn)有的需求;而計劃的設(shè)計則需要考慮未來的功能擴展。演進的設(shè)計推崇盡快地實現(xiàn),追求快速確定解決方案,快速編碼以及快速實現(xiàn);而計劃的設(shè)計則需要考慮計劃的周密性,架構(gòu)的完整性并保證開發(fā)過程的有條不紊。
美團外賣iOS客戶端,在多端復(fù)用的立項初期面臨著多個關(guān)鍵點:頻道入口與獨立應(yīng)用的復(fù)用,外賣平臺的搭建,兄弟業(yè)務(wù)的接入,點評外賣的協(xié)作,以及架構(gòu)遷移不影響現(xiàn)有業(yè)務(wù)的開發(fā)等等,因此權(quán)衡后我們使用“演進式架構(gòu)為主,計劃式架構(gòu)為輔”的設(shè)計方案。不強求歷史代碼一下達到終極完美架構(gòu),而是循序漸進一步一個腳印,滿足現(xiàn)有需求的同時并保留一定的擴展性。
演進式架構(gòu)推動復(fù)用
術(shù)語解釋
- Waimai:特指『美團外賣』App,泛指那些獨立App形式的業(yè)務(wù)入口,一般為project。
- Channel:特指『美團』App中的外賣頻道,泛指那些以頻道或者Tab形式集成在主App內(nèi)的業(yè)務(wù)入口,一般為Pods。
- Special:指將Waimai中的業(yè)務(wù)代碼與原有工程分離出來,讓業(yè)務(wù)代碼成為一個Pods的形態(tài)。
- 下沉:即下沉到下層,這里的“下層”指架構(gòu)的基層,一般為平臺層或通用層。“下沉”指將不同上層庫中的代碼統(tǒng)一并移動到下層的基層庫中。
在這里先貼出動態(tài)的架構(gòu)演進過程,讓大家有一個宏觀的概念,后續(xù)再對不同節(jié)點的經(jīng)歷做進一步描述。
原始復(fù)用架構(gòu)
如圖4所示,在過去一兩年,因為技術(shù)棧等原因我們只能采用比較保守的代碼復(fù)用方案。將獨立業(yè)務(wù)或工具類代碼沉淀為一個個“Kit”,也就是粒度較小的組件。此時分層的概念還比較模糊,并且以往的工程因歷史包袱導(dǎo)致耦合嚴重、邏輯復(fù)雜,在將UGC業(yè)務(wù)剝離后發(fā)現(xiàn)其他的業(yè)務(wù)代碼無法輕易的抽出。(此時的代碼復(fù)用率只有2.4%。)
鑒于之前的準備工作已經(jīng)完成,多端基礎(chǔ)庫已經(jīng)一致,于是我們不再采取保守策略,豐富了一些組件化通信、解耦與過渡的手段,在分層架構(gòu)上開始發(fā)力。
業(yè)務(wù)復(fù)用探索
在技術(shù)棧已統(tǒng)一,基礎(chǔ)層已對齊的背景下,我們挑選外賣核心業(yè)務(wù)之一的Store(即商家容器)開始了在業(yè)務(wù)復(fù)用上的探索。如圖5所示,大致可以理解為“二合一,一分三”的思路,我們從代碼風格和開發(fā)思路上對兩邊的Store業(yè)務(wù)進行對齊,在此過程中順勢將業(yè)務(wù)類與技術(shù)(功能)類的代碼分離,一些通用Domain也隨之分離。隨著一個個組件的拆分,我們的整體復(fù)用度有明顯提升,但開發(fā)效率卻意外的受到了影響。多庫開發(fā)在版本的發(fā)布與集成中增加了很多人工操作:依賴沖突、lock文件沖突等問題都阻礙了我們的開發(fā)效率進一步提升,而這就是之前“關(guān)于組件化”中提到的副作用。
于是我們將自動發(fā)版與自動集成提上了日程。自動集成是將“組件開發(fā)完畢到功能合入工程主體打出測試包”之間的一系列操作自動化完成。在這之前必須完成一些前期鋪墊工作——殼工程分離。
殼工程分離
如圖6所示,殼工程顧名思義就是將原來的project中的代碼全部拆出去,得到一個空殼,僅僅保留一些工程配置選項和依賴庫管理文件。
為什么說殼工程是自動集成的必要條件之一?
因為自動集成涉及版本號自增,需要機器修改工程配置類文件。如果在創(chuàng)建二進制的過程中有新業(yè)務(wù)PR合入,會造成commit樹分叉大概率產(chǎn)生沖突導(dǎo)致集成失敗。抽出殼工程之后,我們的殼只關(guān)心配置選項修改(很少),與依賴版本號的變化。業(yè)務(wù)代碼的正常PR流程轉(zhuǎn)移到了各自的業(yè)務(wù)組件git中,以此來杜絕人工與機器的沖突。
殼工程分離的意義主要有如下幾點:
- 讓職能更加明確,之前的綜合層身兼數(shù)職過于繁重。
- 為自動集成鋪路,避免業(yè)務(wù)PR與機器沖突。
- 提升效率,后續(xù)Pods往Pods移動代碼比proj往Pods移動代碼更快。
- 『美團外賣』向『美團』開發(fā)環(huán)境靠齊,降低適配成本。
圖7的第一張圖到第二張圖就是上文提到的殼工程分離,將“Waimai”所有的業(yè)務(wù)代碼打包抽出,移動到過渡倉庫Special,讓原先的“Waimai”成為殼。
第二張圖到第三張圖是Pods庫的內(nèi)部消化。
前一階段相當于簡單粗暴的物理代碼移動,后一階段是對Pods內(nèi)整塊代碼的梳理與分庫。
內(nèi)部消化對齊
在前文“多端復(fù)用概念圖”的部分我們提到過,所謂的復(fù)用是讓多端的project以Pods的方式接入統(tǒng)一的代碼。我們兼容考慮保留一端代碼完整性,降低回接成本,決定分Subpods使用階段性合入達到平滑遷移。
圖8描述了多端相同模塊內(nèi)的代碼具體是如何統(tǒng)一的。此時因為已經(jīng)完成了殼工程分離,所以業(yè)務(wù)代碼都在“Special”這樣的過渡倉庫中。
“Special”和“Channel”兩端的模塊統(tǒng)一大致可分為三步:平移 → 下沉 → 回接。(前提是此模塊的業(yè)務(wù)上已經(jīng)確定是完全一致。)
平移階段是保留其中一端“Special”代碼的完整性,以自上而下的平移方式將代碼文件拷貝到另一端“Channel”中。此時前者不受任何影響,后者的代碼因為新文件拷貝和原有代碼存在重復(fù)。此時將舊文件重命名,并深度優(yōu)先遍歷新文件的依賴關(guān)系補齊文件,最終使得編譯通過。然后將舊文件中的部分差異代碼加到新文件中做好一定的差異化管理,最后刪除舊文件。
下沉階段是將“Channel”處理后的代碼解耦并獨立出來,移動到下層的Pods或下層的SubPods。此時這里的代碼是既支持“Special”也支持“Channel”的。
回接階段是讓“Special”以Pods依賴的形式引用之前下沉的模塊,引用后刪除平移前的代碼文件。(如果是在版本的間隙完成固然最好,否則需要考慮平移前的代碼文件在這段時間的diff。)
實際操作中很難在有限時間內(nèi)處理完一個完整的模塊(例如訂單模塊)下沉到Pods再回接。于是選擇將大模塊分成一個個子模塊,這些子模塊平滑的下沉到SubPods,然后“Special”也只引用這個統(tǒng)一后的SubPods,待一個模塊完全下沉完畢再拆出獨立的Pods。
再總結(jié)下大量代碼下沉時如何保證風險可控:
- 聯(lián)合PM,先進行業(yè)務(wù)梳理,特殊差異要標注出來。
- 使用OClint的提前掃描依賴,做到心中有數(shù),精準估時。
- 以“Special”的代碼風格為基準,“Channel”在對齊時僅做加法不做減法。
- “Channel”對齊工作不影響“Special”,并且回接時工作量很小。
- 分迭代包,QA資源提前協(xié)調(diào)。
中間件層級壓平
經(jīng)過前面的“內(nèi)部消化”,Channel和Special中的過渡代碼逐漸被分發(fā)到合適的組件,如圖9所示,Special只剩下AppOnly,Channel也只剩下ChannelOnly。于是Special消亡,Channel變成打包工程。
AppOnly和ChannelOnly 與其他業(yè)務(wù)組件層級壓平。上層只留下兩個打包工程。
平臺層建設(shè)
如圖10所示,下層是外賣基礎(chǔ)庫,WaimaiKit包含眾多細分后的平臺能力,Domain為通用模型,XunfeiKit為對智能語音二次開發(fā),CTKit為對CoreText渲染框架的二次開發(fā)。
針對平臺適配層而言,在差異化收斂與依賴關(guān)系梳理方面發(fā)揮重要角色,這兩點在下問的“衍生問題解決中”會有詳細解釋。
外賣基礎(chǔ)庫加上平臺適配層,整體構(gòu)成了我們的外賣平臺層(這是邏輯結(jié)構(gòu)不是物理結(jié)構(gòu)),提供了60余項通用能力,支持無差異調(diào)用。
多端通用架構(gòu)
此時我們把基層組件與開源組件梳理并補充上,達到多端通用架構(gòu),到這里可以說真正達到了多端復(fù)用的目標。
由上層不同的打包工程來控制實際需要的組件。除去兩個打包工程和兩個Only組件,下面的組件都已達到多端復(fù)用。對比下“Waimai”與“Channel”的業(yè)務(wù)架構(gòu)圖中兩個黑色圓圈的部分。
衍生問題解決
差異問題
A.需求本身的差異
三種解決策略:
- 對于文案、數(shù)值、等一兩行代碼的差異我們使用 運行時宏(動態(tài)獲取proj-identifier)或預(yù)編譯宏(custome define)直接在方法中進行if else判斷。
- 對于方法實現(xiàn)的不同 使用Glue(膠水層),protocol提供相同的方法聲明,用來給外部調(diào)用,在不同的載體中寫不同的方法實現(xiàn)。
- 對于較大差異例如兩邊WebView容器不一樣,我們建多個文件采用文件級預(yù)編譯,可預(yù)編譯常規(guī).m文件或者Category。(例如WMWebViewManeger_wm.m&WMWebViewManeger_mt.m、UITableView+WMEstimated.m&UITableView+MTEstimated.m)
進一步優(yōu)化策略:
用上述三種策略雖然完成差異化管理,但差異代碼散落在不同組件內(nèi)難以收斂,不便于管理。有了平臺適配層之后,我們將差異化判斷收斂到適配層內(nèi)部,對上層提供無差異調(diào)用。組件開發(fā)者在開發(fā)中不用考慮宿主差異,直接調(diào)用用通用接口。差異的判斷或者后續(xù)優(yōu)化在接口內(nèi)部處理外部不感知。
圖14給出了一個平臺適配層提供通用接口修改后的例子。
B.多端節(jié)奏差異
實際場景中除了需求的差異還有可能出現(xiàn)多端進版節(jié)奏的差異,這類差異問題我們使用分支管理模型解決。
前提條件既然要多端復(fù)用了,那需求的大方向還是會希望多端統(tǒng)一。一般較多的場景是:多端中A端功能最少,B端功能基本算是是A端的超集。(沒有絕對的超集,A端也會有較少的差異點。)在外賣的業(yè)務(wù)中,“Channel”就是這個功能較少的一端,“Waimai”基本是“Channel”的超集。
兩端的差異大致分為了這5大類9小類:
也不用過多糾結(jié),圖15是最復(fù)雜的場景,實際場合中很難遇到,目前的我們的業(yè)務(wù)只遇到1和2兩個大類,最多2條線。
編譯問題
以往的開發(fā)方式初次全量編譯5分鐘左右,之后就是差量編譯很快。但是抽成組件后,隨著部分子庫版本的切換間接的增加了pod install的次數(shù),此時高頻率的3分鐘、5分鐘會讓人難以接受。
于是在這個節(jié)點我們采用了全二進制依賴的方式,目標是在日常開發(fā)中直接引用編譯后的產(chǎn)物減少編譯時間。
如圖所示三個.a就是三個subPods,分了三種Configuration:
這里有一個問題需要解決,即引用二進制帶來的弊端,顯而易見的就是將編譯期的問題帶到了運行期。某個宏修改了,但是編譯完的二進制代碼不感知這種改動,并且依賴版本不匹配的話,原本的方法缺失編譯錯誤,就會帶到運行期發(fā)生崩潰。解決此類問題的方法也很簡單,就是在所有的打包工程中都配置了打包自動切換源碼。二進制僅僅用來在開發(fā)中獲得更高的效率,一旦打提測包或者發(fā)布包都會使用全源碼重新編譯一遍。關(guān)于切源碼與切二進制是由環(huán)境變量控制拉取不同的podspec源。
并且在開發(fā)中我們支持源碼與二進制的混合開發(fā)模式,我們給某個binary_pod修飾的依賴庫加上標簽,或者使用.patch文件,控制特定的庫拉源碼。一般情況下,開發(fā)者將與自己當前需求相關(guān)聯(lián)的庫拉源碼便于Debug,不關(guān)聯(lián)的庫拉二進制跳過編譯。
依賴問題
如圖17所示,外賣有多個業(yè)務(wù)組件,公司也有很多基礎(chǔ)Kit,不同業(yè)務(wù)組件或多或少會依賴幾個Kit,所以極易形成網(wǎng)狀依賴的局面。而且依賴的版本號可能不一致,易出現(xiàn)依賴沖突,一旦遇到依賴沖突需要對某一組件進行修改再重新發(fā)版來解決,很影響效率。解決方式是使用平臺適配層來統(tǒng)一維護一套依賴庫版本號,上層業(yè)務(wù)組件僅僅關(guān)心平臺適配層的版本。
當然為了避免引入平臺適配層而增加過多無用依賴的問題,我們將一些依賴較多且使用頻度不高的Kit抽出subPods,支持可選的方式引入,例如IM組件。
再者就是pod install 時依賴分析慢的問題。對于殼工程而言,這是所有依賴庫匯聚的地方,依賴關(guān)系寫法若不科學(xué)極易在analyzing dependency中耗費大量時間。Cocoapods的依賴分析用的是Molinillo算法,鏈接中介紹了這個算法的實現(xiàn)方式,是一個具有前向檢察的回溯算法。這個算法本身是沒有問題的,依賴層級深只要依賴寫的合理也可以達到秒開。但是如果對依賴樹葉子節(jié)點的版本號控制不夠嚴密,或中間出現(xiàn)了循環(huán)依賴的情況,會導(dǎo)致回溯算法重復(fù)執(zhí)行了很多壓棧和出棧操作耗費時間。美團針對此類問題的做法是維護一套“去依賴的podspec源”,這個源中的dependency節(jié)點被清空了(下圖中間)。實際的所需依賴的全集在殼工程Podfile里平鋪,統(tǒng)一維護。這么做的好處是將之前的樹狀依賴(下圖左)壓平成一層(下圖右)。
效率問題
前面我們提到了自動集成,這里展示下具體的使用方式。美團發(fā)布工程組自行研發(fā)了一套HyperLoop發(fā)版集成平臺。當某個組件在創(chuàng)建二進制之前可自行選擇集成的目標,如果多端復(fù)用了,那只需要在發(fā)版創(chuàng)建二進制的同時勾選多個集成的目標。發(fā)版后會自行進行一系列檢查與測試,最終將代碼合入主工程(修改對應(yīng)殼工程的依賴版本號)。
以上是“Waimai”的commit對比圖。第一張圖是以往的開發(fā)方式,能看出工程配置的commit與業(yè)務(wù)的commit交錯堆砌。第二張圖是進行殼工程分離后的commit,能看出每條message都是改了某個依賴庫的版本號。第三張圖是使用自動集成后的commit,能看出每條message都是畫風統(tǒng)一且機器串行提交的。
這里又衍生出另一個問題,當我們用殼工程引Pods的方式替代了project集中式開發(fā)之后,我們的代碼修改散落到了不同的組件庫內(nèi)。想看下主工程6.5.0版本和6.4.0版本的diff時只能看到所有依賴庫版本號的diff,想看commit和code diff時必須挨個去組件庫查看,在三輪提測期間這樣類似的操作每天都會重復(fù)多次,很不效率。
于是我們開發(fā)了atomic diff的工具,主要原理是調(diào)git stash的接口得到版本號diff,再通過版本號和對應(yīng)的倉庫地址深度遍歷commit,再深度遍歷commit對應(yīng)的文件,最后匯總,得到整體的代碼diff。
整套工具鏈對多端復(fù)用的支撐
上文中已經(jīng)提到了一些自動化工具,這里整理下我們工具鏈的全景圖。
總結(jié)
- 多端復(fù)用之后對PM-RD-QA都有較大的變化,我們代碼復(fù)用率由最初的2.4%達到了84.1%,讓更多的PM投入到了新需求的吞吐中,但研發(fā)效率提升增大了QA的工作量。一個大的嘗試需要RD不斷與PM和QA保持溝通,選擇三方都能接受的最優(yōu)方案。
- 分清主次關(guān)系,技術(shù)架構(gòu)等最終是為了支撐業(yè)務(wù),如果一個架構(gòu)設(shè)計的美如畫天衣無縫,但是落實到自己的業(yè)務(wù)中確不能發(fā)揮理想效果,或引來抱怨一片,那這就是個失敗的設(shè)計。并且在實際開發(fā)中技術(shù)類代碼修改盡量選擇版本間隙合入,如果與業(yè)務(wù)開發(fā)的同學(xué)產(chǎn)生沖突時,都要給業(yè)務(wù)同學(xué)讓路,不能影響原本的版本迭代速度。
- 時刻對 “不合理” 和 “重復(fù)勞動”保持敏感。新增一個埋點常量要去改一下平臺再發(fā)個版是否成本太大?一處訂單狀態(tài)的需求為什么要修改首頁的Kit?實際開發(fā)中遇到別扭的地方多增加一些思考而不是硬著頭皮過去,并且手動重復(fù)兩次以上的操作就要思考有沒有自動化的替代方案。
- 一旦決定要做,在一些關(guān)鍵節(jié)點決不能手軟。例如某個節(jié)點為了不Block別人,加班不可避免。在大量代碼改動時也不用過于緊張,有提前預(yù)估,有Case自測,還有QA的三輪回歸來保障,保持專注,放手去做就好。
作者簡介
- 尚先,美團資深工程師。2015年加入美團,目前作為美團外賣iOS端平臺化虛擬小組組長,主要負責業(yè)務(wù)架構(gòu)、持續(xù)集成和工程化相關(guān)工作,致力于提升研發(fā)效率與協(xié)作效率。
招聘信息
美團外賣長期招聘iOS、Android、FE高級/資深工程師和技術(shù)專家,Base北京、上海、成都,歡迎有興趣的同學(xué)投遞簡歷到chenhang03#meituan.com。
總結(jié)
以上是生活随笔為你收集整理的美团外卖iOS多端复用的推动、支撑与思考的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ReactiveCocoa中潜在的内存泄
- 下一篇: 期望最大化(EM)算法真如用起来那么简单