高德地圖:崩潰率從萬分之八降到十萬分之八的架構奧祕

近幾年來,高德地圖業務發展迅猛,團隊規模迅速擴張,代碼體量急劇增加,爲了提高團隊高效並行作戰的能力,端上做了一系列架構升級。2018年通過雙端融合、組件化、研發平臺搭建等技術實踐,使得發版效率提升50%, App崩潰率從萬分之八降到十萬分之八。本文整理自ArchSummit 全球架構師峯會(深圳站)2019 峯會演講,主要分享在一系列架構升級改進中,高德地圖的具體做法、經驗和思考。

大家好,我是來自高德地圖的郝仁杰,本次分享的主題是“高德地圖App架構演化與實踐”。2018年,我們通過架構的演進將發版週期縮短至一半,整個 App 的崩潰率從萬分之八降低至十萬分之八。在正式開始介紹之前,我先來簡單介紹下高德。

背景介紹

高德是國內數字地圖內容、導航和位置服務解決方案提供商。目前在端上,分爲手機和車機兩條主線。近年來,高德業務迅猛發展,人員規模迅速擴張,代碼體量急劇增加,爲了提高團隊高效並行作戰的能力,端上做了C++多端融合和動態化能力建設。

回顧近幾來高德地圖App架構的演進歷程:2014年,手機端上只有幾十個研發, Android 和 iOS 端由原生單體架構實現;2015年,地圖引擎下沉C++,實現了手機和車機的多端融合;2016年,端上啓動了動態UI框架的開發,爲未來業務的動態化鋪路;2017年,動態UI框架建設完成,具備了運行靜態頁面的能力;到了2018年,手機端已經成長爲擁有數百研發規模的團隊,雙端代碼量也已經達到數百萬行,架構要如何繼續演化來提高團隊高效並行作戰的能力,來支撐並賦能業務快速發展呢?

問題現狀

爲了讓業務開發有節奏的進行,項目上每年會制定一些公車計劃。公車就是每個App版本,版本里帶的產品功能就是公車上的貨物,公車計劃即每年的發版計劃。按照計劃,公車會在指定的時間把組裝好的貨物拉走。

2018年初,由於雙端代碼差異較大、耦合嚴重、複用率低、職責不清晰、平臺工具簡陋等問題,公車無法按照計劃拉走貨物。工具落後,貨物組裝慢且質量差,無法如期交貨,迫使公車等待,導致整個發版週期長達3個月,崩潰率高達萬分之八,公車變成了僞公車。

爲了解決這些問題,使僞公車變爲真公車,需要做到穩定、並行和高效。端上通過以下三種方式達到該目的,一是雙端融合,如上圖,藍色部分上漂動態UI,下沉C++,以及Android、iOS雙端拉齊,減少差異,提高可維護性;二是選擇組件化方案,分而治之,解除耦合,提高複用率,做到並行、高效;三是搭建研發中臺,工具升級,流程自動化以及風險質量管控,提升效率和穩定性。

執行方案

雙端融合

2015年,我們通過地圖引擎下沉C++,實現了手機、車機的多端融合,同理,可將部分功能下沉C++;通過2017年建成的動態UI框架,可將部分業務上漂到動態UI;對於既不能上漂也不能下沉的,通過雙端拉齊做到融合。

那麼,什麼樣的場景適合下沉到C++呢?一,需要有穩定的邏輯,不經常變化;二是不強依賴原生;三,對性能要求較高。舉例來說,導航邏輯,地圖從開始建立到現在已經打磨出一套非常核心穩固的邏輯,這部分邏輯可以下沉到C++。

哪些場景又適合上漂到動態UI呢?一,對性能要求不高;二,經常易變的業務代碼,比如產品的UI需求;三,不強依賴於原生能力。

對於既不能下沉,也不能上漂的功能,選擇雙端拉齊:對性能有一定要求;強依賴原生的能力;需要支撐一些原生業務。例如,高德地圖的頁面框架,雖然 Android 和 iOS 端有原生的頁面框架,但地圖類應用和普通應用不太一樣,地圖類應用的主要功能是圍繞着一張地圖進行,這張地圖上面的元素非常豐富,數據量非常龐大,內存佔用較大,如果採用原生頁面框架進行開發,就意味着每切換一個頁面就得創建一張新地圖,這對手機端這種資源緊缺的環境來說是非常浪費的,對於低端機型來說是不可接受的。

另外,地圖應用從一個頁面切換到另一個頁面,或者從一個場景切換到另一個場景,並不是完全不同的兩張圖切換,而僅僅是一張地圖的不同狀態轉換,此時,如果額外創建一張新的地圖,顯然是極大的浪費。所以,對於地圖類應用,我們建設了自己的頁面框架:以單系統頁面控制器多視圖切換的方式實現。由於原來都是單體開發,Android 和 iOS 只關注自身特性,兩邊的實現不太一樣,跳轉規則、功能特性均有差異,我們通過分析雙端的規則、特性,借鑑雙端各自的優點,設計了一套統一的規則、特性,實現了雙端融合。

下面簡單介紹高德地圖頁面框架的融合方案:

如上圖,左邊的 Activity 是 Android 的系統頁面控制器,右邊的 UIViewController 是 iOS 的系統頁面控制器,通過虛線連接比較,我們發現兩端的頁面狀態設計基本相同。所以,我們在設計自己的頁面框架時沿用了這些系統頁面狀態,同時從命名上也保持一致,這樣可以讓 Android 和iOS 原生開發的同學更容易理解和上手。

此外,我們吸取了雙端各自的優點。比如,Android 端頁面有四種啓動模式,但是 iOS 端並沒有這些,我們就把 Android 的四種啓動模式運用到了 iOS 端;iOS端有 Present 特性,但是Android 端沒有,那麼也把這種特性融合到 Android 端的頁面框架中;最後,還有一些小設計,比如 Android 的 onResult 設計,也可以借鑑融合到 iOS 端。

首先,介紹下四種啓動模式,這是安卓特有的。第一種是 Standard 模式,這個模式和棧的行爲是一樣的,就是標準的 Push 和 Pop;第二種是 SingleTop 模式,當向一個頁面棧壓入另一個頁面時,如果該頁面已經在棧頂,那麼將不會創建一個新的頁面實例 C 放到棧頂,不會變成 ABCC 這樣的方式,而僅僅是通知當前棧頂的 C 頁面做一個數據更新;第三種是SingleInstance模式,當以 SingleInstance 模式 Push 一個頁面時,如果該頁面已經在棧中,那麼就把它從棧中帶到棧頂;最後一種是 SingleTask 的模式,這和原生系統略有差別,因爲我們目前是基於單頁面控制器的方式實現的,當以 SingleTask 的方式 Push 一個頁面到頁面棧時,如果該頁面已經在棧中,頁面框架會把其之上的所有頁面全部清除出棧,使其成爲新的棧頂,這就是四種啓動模式。

接下來,簡單介紹iOS的Present特性。當頁面棧頂的 C 頁面 Present D 頁面時,D 頁面並沒有被加入到 ABC 頁面棧中,而是變成了 C 頁面的一個附屬,當 D 頁面要消失時,同樣也是通過C 頁面的 dismiss 移除掉。這裏有些限制,每個頁面僅可以 Present 一個頁面,這個頁面可以是一個普通頁面,也可以是一個導航頁面,那麼導航頁面是什麼呢?大家可以理解成一個新的頁面棧(功能類似UINavigationController),其上可以添加其它頁面。如果 D 頁面是導航頁面,就可以在其上 Push 其它頁面,如果業務流程有一個主流程,一個分支流程,就可以採用這種方式實現。

最後是Android 的 onResult 特性,實現了頁面間數據返回的解耦,如上示例代碼就是大致的實現原理。具體來說,從 A 頁面跳轉到 B 頁面,那麼 B 頁面執行了一段邏輯之後,A 希望得到執行結果,如果按照原來 iOS 的實現方式,只能通過監聽 Listener 或 Delegate 等方式將 B 頁面的執行結果返回給 A 頁面。當 iOS 的頁面框架實現了這個特性, A 頁面就不需要額外註冊 B 頁面的 Listener 或 Delegate 了,只需重寫自己的 onResult 方法並處理結果即可,這樣既可以實現頁面解耦,又方便了業務同學開發。

接下來,舉個高德地圖手機端上的具體實例。

有這樣一個搜索場景,從一個具體地理位置詳情頁可以跳轉到以它爲中心的搜周邊頁,在搜周邊頁中又可以跳轉到另一個具體地理位置詳情頁,接着可以跳轉到新的搜周邊頁,以此遞歸循環,但是返回時,產品希望僅返回到之前搜索過的具體地理位置詳情頁,略去搜周邊頁。如上右側圖片展示,查詢順序是:7天優品酒店詳情頁 -> 7天優品酒店搜周邊頁 -> 火驢火燒肉亭詳情頁 -> 火驢火燒肉亭搜周邊頁;返回順序是:火驢火燒肉亭搜周邊頁 -> 火驢火燒肉亭詳情頁 -> 7天優品酒店詳情頁。中間的7天優品酒店搜周邊頁被去掉了。

在 iOS 頁面框架未實現 launch mode 前,火驢火燒肉亭詳情頁在跳轉到火驢火燒肉亭搜周邊頁前,需要自行遍歷當前頁面棧,將7天優品酒店搜周邊頁從頁面棧中移出後,再跳轉到火驢火燒肉亭搜周邊頁,以此保證產品邏輯的正確性。在實現了 launch mode 之後,火驢火燒肉亭詳情頁僅需以 SingleInstance 的方式打開火驢火燒肉亭搜周邊頁即可,頁面框架會自動將之前的7天優品酒店搜周邊頁調到棧頂,並將該搜周邊頁的內容刷新爲火驢火燒肉亭搜周邊。極大簡化了 iOS 端業務同學的開發成本,規範了 iOS 頁面跳轉規範,結束了由業務自行操作頁面棧的混亂時代,同時雙端技術能力的融合也爲上層動態UI業務提供了一致性的體驗。

上面,我們介紹了雙端融合方案的三種方式,也舉例說明了其帶來的效果。下沉C++,實現兩套代碼合一,解決了一致性問題,提高了性能,但同時也提高了開發門檻,適用於多年沉澱的核心邏輯;上漂動態UI,同樣解決了雙端一致性問題,性能會稍有損失,但降低了開發門檻,使得開發速度得到提升,適用於頻繁變動的業務場景;雙端拉齊則是借鑑了雙端優勢,做到互相融合。

組件化

我們做了一些團隊組件化方案的選型和參考,例如手淘的 Atlas、Beehive,網易的 LDBusMediator 等,由於這些組件化方案都比較成熟,這裏不再贅述。它們都包含五個概念:容器、模塊、生命週期、頁面路由和對外服務(通信),我們重新命名了這些概念使其更加形象化。

容器,負責管理模塊;模塊,是一個獨立的功能單元,可以獨立編譯;微應用,管理模塊的生命週期,對於一個手機操作系統,是爲每個應用派發生命週期,對於一個單獨的應用,是爲每個模塊派發生命週期,就像一個應用管理着很多微應用一樣,因此我們取了這個形象的名字;頁面路由,負責進行URL的解析和頁面跳轉;微服務,模塊中的邏輯功能,同時提供對外服務。

我們對容器在設計進行了一些改造,如上右半邊圖,模塊被虛化了,被定義成了一個物理概念(即一個獨立代碼倉庫),邏輯上拆分爲微應用、微服務和頁面路由,容器不再管理模塊,而是直接管理這三個元素。之所以這樣做,是因爲我們希望業務更關注自身需要的服務是什麼,而不是它在哪個模塊,這些也是借鑑了安卓的組件化思想。

接下來,我們詳細介紹下微應用生命週期的設計,如上圖,微應用在 iOS 端參考的是 UIApplicationDelegate 的生命週期,而在 Android 端參考的是Activity 的生命週期。做這樣的參考選擇,原因有三:一,高德地圖內的應用場景大都依賴前後臺切換的事件做一些邏輯處理;二,iOS 的 UIApplicationDelegate 作爲應用的生命週期,同時支持前後臺切換,完全吻合高德地圖的場景;三,Android 選擇 Activity 是因其組件化的思想,在 Android 的設計中,Application 已經弱化成了一個特殊進程的概念,並不能代表一個應用,且高德地圖是基於單 Activity 實現的(上面介紹頁面框架時提到過),通過 Activity 的 onStop,onRestart 生命週期中做些邏輯處理,即可判斷出應用是否爲前後臺切換。這樣,去除圖中虛線框中的生命週期後,雙端得到了統一的生命週期,如下左半部分圖:

對於虛線中差異化的部分(如上右半部分圖),設計爲擴展的生命週期,做到抽象相同、擴展差異,既統一了通用生命週期,也支持了雙端各自的特性。

對於微服務,我們定義了一個通信規範,只能通過接口方法,不能直接調用實現。定義微服務主要是希望UI展現與業務邏輯能夠分離,並讓業務邏輯服務化,不僅服務於當前頁面,也能夠服務更多頁面,提高代碼的複用率,降低維護成本。

有了容器框架,代碼便可以抽成一個個獨立的模塊單元,但模塊應該放在那裏,上下依賴關係是什麼,還需要對模塊進行分層、分組,下圖爲分層、分組後的整體架構:

通過容器建設,架構分層、分組,我們實現了組件化,解除了模塊間耦合,提高了代碼複用率,爲後面的高效並行打好基礎。分而治之的思想,組件化的“分”也是爲後面的“治”做好鋪墊。

搭建研發中臺

研發中臺應該有哪些功能,可以結合組件化和公車流程來分解,如下圖:

主流程是公車流程,分爲:需求收集、需求串講、開發、合版、提測、灰度發佈和正式上線。開發流程可以分解爲更細的建立迭代、選擇模塊、功能開發、模塊構建和安裝包構建。這裏解釋下迭代的概念,即一個發版週期內的功能開發。組件化實現了功能解耦,使得不同業務團隊可以在開發階段創建自己的迭代並行開發,開發完成後在規定的時間段進行合版。提測流程可以分成模塊集成、安裝包構建和集成測試,其中模塊集成是以產物的方式進行集成。測試通過後,通過客戶端發佈流程,進行灰度發佈驗證,灰度通過後,再進行正式上線,上線之後,我們會對崩潰、性能等維度進行監控。通過流程拆解,我們整理出了研發中臺的完整功能:

研發中臺建設完成後,我們實現了研發流程、測試流程以及發佈流程的自動化,提高了人效。另外,通過質量管控,提高了穩定性;通過流程管控,約束了可能產生的風險。

主副收益

首先,通過雙端融合、組件化、中臺建設提升了代碼穩定性,實現了流程自動化,做到了開發階段的並行,使發版週期縮短到原來的一半,從僞公車變成真公車。

其次,通過質量優化,讓崩潰率從萬分之八降低到十萬分之八:雙端融合減少了一致性問題;架構合理化提高了可維護性;關鍵流程管控,減少了風險源頭;通過質量掃描,解決了頭部質量問題,通過崩潰監控,解決頭部崩潰問題。

然後,通過升級編譯腳本,支持並行編譯;通過模塊化,基於產物構建安裝包,大大降低編譯時長,從原來的40多分鐘降至現在的8分鐘。

最後是包大小優化,iOS端從 146M 減到 123M,純減量達48M,這主要是通過編譯優化,資源雲化,功能合併(分層、分組),svg替代png小圖標,刪除無用圖片和代碼實現等手段實現。其中,資源雲化主要是指將啓動時的非必要資源放在雲端,需要時再進行動態加載。

經驗教訓

在組件化以後,編譯模式發生了一些變化,模塊在集成前提前生成了產物,這些變化同時帶來了一些問題,比如二進制兼容問題。以枚舉功能爲例,在模塊化後,A模塊依賴B模塊中定義的枚舉,在A模塊生成產物後,B模塊的枚舉定義發生了變化,A中使用的枚舉值含義可能發生變化,如下圖:

爲了解決該問題,我們制定了一些開發規範:對於枚舉的定義,不允許刪除任何已定義的枚舉值,不允許從中間插入任何枚舉值,如果一定要添加,只能在末尾添加,以此來解決二進制兼容性問題。當然,除了枚舉的問題,還有宏定義等引起的二進制兼容性問題,此處不一一詳述。

此外,Android 端還可能出現代碼註解丟失問題。編譯期註解僅存在於編譯階段,模塊化後,產物中無法保存註解信息,導致產物集成時,由於找不到註解信息而無法進行全局註冊。爲此,我們做了一些自定義 APT 插件,在註解處 階段生成Java數據類的同時也存儲一份註解信息,這樣在集成階段就可以根據註解信息進行全局註冊。

未來展望

2018年,高德客戶端通過一系列架構治理,從僞公車變成了真公車,但這只是近幾年架構演進的一個階段性成果。未來,我們要發揮動態UI的優勢,讓業務真正動態化起來,從公車時代跨入到Feature Team時代,讓公車變成一條條公路,每個Feature Team就是一個小汽車,按照自己的節奏裝好貨物後,就可以在修好的公路上自由的行駛,更好地做到靈活、並行和高效。

嘉賓介紹

郝仁杰,高德地圖無線開發專家。十餘年移動客戶端開發經驗,曾深度參與 Nokia S60 Contacts,YY 語音,360 手機衛士的研發維護工作,對 Symbian、Android、iOS 系統有較深的理解。目前在高德地圖負責 Android、iOS 端的基礎架構,2018年,帶領團隊實現了端上的 Bundle 化等一系列架構升級的開發工作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章