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