百度App iOS工程化實踐: EasyBox破冰之旅

本文做者:yanxin1563html

本文做者:git

Yunpeng-基礎技術github

前言swift

百度App從單一的搜索工具發展到今天以搜索和Feed流爲雙引擎的綜合性內容消費服務平臺,其複雜程度已然不可同日而語矣。 做爲一個日活過億的超級App,業務規模龐大,相關技術人員超過千人,客戶端支持主流的移動技術,涉及近百業務方,技術形態複雜,各類組件近三百個,代碼百萬量級,由此帶來的工程化問題是技術團隊的一個極大挑戰。數組

項目的膨脹致使了不少不起眼的小問題被無限放大,組件管理不規範、編譯時間長、工程文件合併衝突、Xcode默認非完全編譯隔離等等問題,致使開發人員在開發環境上耗費了大量時間。目前業界較流行的工具對於大規模工程的支持力度相對較弱,實踐起來老是有些掣肘,難以達到理想狀態。xcode

EasyBox的誕生,就是致力於爲超級App量身打造一套現代、高效、優雅的研發工具鏈。緩存

這篇文章的主要目的是站在工具鏈的角度上,分享一下咱們在實踐工程化過程當中一些經驗。安全

概述服務器

EasyBox主體由工程組裝器(Installer)、多倉庫管理工具(MGit)、二進制管理工具(LFS)三部分構成,分別負責工做區的構建(組件依賴分析、工程的生成與組合)、源碼倉庫的管理以及二進制的管理。EasyBox架構圖:微信

由多倉庫管理工具克隆所需倉庫源碼,由二進制管理工具下載二進制包,而後組裝器根據描述表生成對應工程,組合層級並最終生成Workspace。簡化工做流程示意圖:

實踐

EasyBox誕生的過程本質上是工程化逐步深刻的過程。分而治之的道理你們都明白,但這並不意味着工程化就是簡單的拆庫重組。其目的是要對項目工程進行合理化改造,讓開發人員可以快速理解工程架構並進入開發狀態,避免開發人員在開發環境上花費過多時間,從而提升編碼、測試等階段的研發效率。在這個過程當中咱們在規範組件管理與使用、強化工程能力、提高編譯速度這三個方面不斷優化,最終造成EasyBox獨特的優點。

1. 規範組件管理與使用

組件(源碼/二進制)統一使用描述表(boxspec)描述,依賴與API管理均由描述表惟一決定,配合編譯隔離使得組件邊界劃分明確。組件的版本號嚴格遵照語義化版本(Semantic Versioning)規範,組件新版本發佈時,需通過持續集成分析API變化等一系列檢查以後方可完成發佈。

破窗理論 環境中的不良現象若是被聽任存在,會誘令人們仿效,甚至變本加厲。

組件基於源碼發佈來完成二進制發佈及接口發佈,以確保安全性,方便進行源碼迴歸校驗以及必要時基於同一節點發布不一樣類型的二進制,接口發佈用於完成矩陣產品的組件替換。發佈版本的描述表與二進制文件交由專門的文件服務器管理,並禁止二進制文件入源碼倉庫以免倉庫膨脹。更多源碼與二進制管理細節參見後文介紹。

2. 強化工程能力

2.1 組件的獨立與隔離

與業界其餘大多數工具不一樣,EasyBox採起了每一個組件都拆分爲獨立工程的方案,同時也將不一樣的組件的編譯產物放在了不一樣的目錄下,這樣作的目的是確保完全的編譯隔離

這裏實際上是源自Xcode的遺留的兩個坑:

  1. 同一個工程的OC/C/C++文件就算不在同一個Target下且沒有依賴關係也能夠互相訪問(Swift是不容許的)。

  2. Xcode會自動將編譯產物所在的文件夾(BUILD_PRODUCTS_DIR)添加到  FRAMEWORK_SEARCH_PATHS 中,而同一個Workspace下的編譯產物默認是在同一文件夾下的。

這兩個坑都會打破編譯隔離,後者更會致使在不一樣開發者的電腦上編譯結果不一樣,時常出現本身編譯過了而別人編譯不過的狀況,這是其實因爲組件編譯次序不一樣致使的。

但因爲Xcode工程文件一直是飽受詬病的設計,多人協做時工程文件合併衝突簡直就是一場噩夢,因此咱們採用了組件配置表的方案來完成去工程文件化,每一個組件經過配置一個boxspec文件(相似於CocoaPods的podspec),組件源碼會根據實際目錄映射對應工程結構,同時生成一個xcfilelist來維護當前組件所配置的資源列表,並最終生成工程文件。該工程文件不被Git追蹤,從而避免合併衝突問題。

編譯隔離能夠帶來不少好處,好比組件邊界必定是明確的,幾百個組件組合在一塊兒能不能編譯過再也不看"運氣"。它限制了組件修改時所影響的範圍,這將有助於組件在不一樣環境(App)下編譯構建、多端複用,使得輸出時開發者對於組件的依賴是有預期的。

另外配合接口發佈及組件化中的協議解耦,使得組件輸出時能夠選擇性的只攜帶其餘組件的接口(而非實現),從而很容易地完成依賴組件的剝離與替換。boxspec描述表示例:

自動生成組件工程示例:

 

2.2 多倉庫的拆分與管理

代碼集中管理致使主倉庫愈來愈臃腫,並且代碼權限問題難以管理,這對於大團隊的開發而言是個很是痛苦的事情,很容易出現組件在負責人不知道的狀況被其餘人合入代碼,加之必定的組件易手率,狀況會變得更加糟糕。

熵增原理 在天然狀況下,一切物質都將趨向於無序。

爲解決這個問題,咱們將各組件拆分爲獨立倉庫,完成物理隔離,作到入庫權限收緊,物各有主。另外這對於多產品複用起了相當重要的做用,也是中臺化建設的必要條件。在實際實踐過程當中,爲了不某些倉庫過於瑣碎,也會出現一個倉庫多個組件的狀況,但並不影響大局。

多倉庫的拆分從某種程度上加重了工程的複雜度,開發者不可避免地出現須要操做多個倉庫的狀況,這時直接使用Git操做成本高且易出錯,爲此咱們專門設計了多倉庫管理工具(MGit, Android/iOS雙端共用)。與Android系統源碼多倉庫管理工具Repo不一樣,MGit保持了Git的大多數指令和用法,同時在內部執行時保證多倉庫操做的安全性,在執行風險操做時作出必要提醒。開發者只需使用  mgit  替代  git  命令便可完成多倉庫操做,這樣既保持了大多數開發者的使用習慣,又能夠安全方便地同時操做多個倉庫。同時咱們利用Gerrit的topic機制並加以改造,實現多倉庫的分組提交以確保原子性入庫,進而保證自動打包機制的正常運做。MGit與Repo指令對比圖:

MGit使用示例:

多倉庫帶來一個最核心的問題是組件節點同步問題。當不一樣組件的源碼處於不一樣節點,很容易出現編譯不過或功能不正常等問題。能夠經過採用『同名分支原則』來規避這個問題,即將全部要開發的倉庫保持在同一個分支,再配合其餘組件的版本依賴管理,一塊兒保證各組件節點的匹配。這個規則很是簡單易記,在大團隊推廣起來也不會有什麼成本。

倉庫嵌套問題 

因爲歷史遺留緣由,咱們在拆倉庫的初期選擇了在組件原有位置建立倉庫,最初設計上考慮不周給咱們帶來了不小的麻煩,不一樣分支的gitignore的不一樣會引發文件追蹤狀態的錯亂,後來便採用將源碼倉庫進行平鋪,以免這個問題。

2.3 層級的動態劃分與構建

大型項目基本都會走上分層架構之路,分層帶來的好處也是顯而易見的,軟件架構更加清晰,規範更容易確立,而層級的單向訪問也有助於下降工程複雜度。分層設計本質上是對開閉原則的踐行,這一原則一般是咱們對架構設計時的主導原則之一,其目的是讓軟件更易於擴展,限制每次修改所影響的範圍。具體就是將軟件劃分爲一系列組件,並將這些組件按層級進行組織,使得下層組件不會由於上層組件的修改而受到影響。

因此與其餘工具不一樣的是,EasyBox選擇在設計上支持層級的劃分,咱們但願EasyBox不只僅承擔包管理器的做用,也起到幫助架構師規範好整個項目的做用。隨着團隊的擴大,依賴不合理的問題更加顯著,不少事情再也不是簡單的給個規範、喊一嗓子就能夠解決的,這時咱們傾向於制定強硬的規則,來確保不會出現明顯的問題。分層的設計也可讓團隊新成員快速理解工程架構設計,時刻提醒系統邊界的重要性,同時創建對依賴的約束限制

"立法" 要遠比 "道德規範" 來的直接有效。

約束的創建能夠規避不少問題,舉個例子,PM要求對某個視圖的展現事件打點,若是對組件化理解不深或者偷懶的話,很容易直接在UI庫裏直接進行打點,而這將爲後續組件的複用帶來很大的問題。百度App現行架構圖(上層組件能夠訪問下層組件,不可逆向訪問):

層級配置示例:

自動生成工程示例:

在組合構建過程當中,不管是組件仍是子組件,都採用了直接連接到最終產物(App或Dynamic Framework)的作法,其緣由是靜態庫之間的合併風險是很高的,好比符號重複時會僅僅會給出一個警告,而後觸發自動裁剪。

force_load問題 

若是App兼容iOS8的話,蘋果對於主包二進制有大小限制,這時能夠將底層Layer改成動態庫來減小主包二進制體積,此時一些C/C++的組件若是出現跨層調用時是須要force_load的,實際應用過程當中應儘量地使用OC封裝這些庫而避免上層業務直接使用這些組件,儘量的少使用force_load,從而避免包體積增大。

 

3. 提高編譯速度

3.1 組件二進制化

業務的膨脹致使百度App代碼激增,這大幅拖累了編譯速度,僅主業務(拋開幾十家業務方及ffmpeg、opencv之類的重量級三方庫)編譯時長也接近20分鐘(13' RMBP),因此咱們決定採用二進制化方案來解決這個問題,即由集羣將組件打包爲二進制並上傳至文件服務器,開發時僅保留須要開發的組件的源碼,其餘各組件均以二進制存在。經過二進制化,正常狀況下(1至3個處於開發模式的組件)全量編譯時間壓縮至2分鐘(13' RMBP)之內,增量編譯速度也明顯加快,而對於工程文件的緩存也能夠有效減小全量編譯的次數。

宿主工程的配置是由Boxfile、Boxfile.overlay、Boxfile.local三個文件配合完成的,配置生效優先級Boxfile.local > Boxfile.overlay > Boxfile。overlay和local格式相同,都是用於開發聯調階段使用的臨時配置文件,用於二進制源碼的切換,區別在於overlay被git追蹤,用於多人協同開發以及持續集成打包,而local則不被git追蹤,僅用於本地調試。當二進制切回開發模式時,若是該倉庫分支不存在,會根據當前組件版本節點(而不是master)建立對應分支,以保證分支的起始節點同步。Boxfile.overlay配置示例:

可視化配置工具示例:

二進制失效問題 

二進制化對組件接口層的穩定是有很強的要求的,組件接口層應當儘量穩定,尤爲注意宏/枚舉等聲明改動引發其餘組件二進制失效問題,接口層儘量採用增量擴展的形式(舊接口標記廢棄)。此時建設監測機制確保版本號的正確性就顯得尤其重要,咱們經過Clang插件來完成對API變更的監控,在組件發佈以前進行校驗。

二進制化還會帶來一個很大的問題就是給開發者的調試帶來不便。Java、JS等語言都會有很完善的源碼映射(Source Map)機制來彌補打包後帶來的調試問題,而對於OC/Swift來講,這方面的建設倒是很是少見的。用過Carthage的同窗都知道,Carthage能夠從工程單步調試進入到源碼裏面的,可是這僅侷限於本地編譯出來的二進制,並且也只能從外部經過單步調試進入。而咱們但願達到的效果是:

二進制包由集羣編譯打包,本地開發時使用二進制文件,工程根據配置自動導入源碼完成源碼映射,源碼不參與編譯,但斷點調試依然有效。

這裏要先理解斷點的本質是什麼,斷點實際上是一個含有觸發條件的座標,斷點 = 源文件位置 + 代碼行數 + 觸發條件

lldb下經過  breakpoint list  查看斷點信息:

集羣編譯和本機編譯的區別在於源文件的位置,因此只要保證源文件位置相同,就能夠達到理想效果。能夠藉助編譯參數  -fdebug-prefix-map  來完成源文件位置的匹配,在集羣編譯時經過該參數將組件源碼目錄指向  /tmp/easybox/$(VIRTUAL_ID)  (VIRTUAL_ID是根據組件信息與時間戳生成的定長字符串)。當EasyBox須要進行源碼映射時,只需導出一份對應時間節點的源碼,而後將該源碼目錄軟鏈到  /tmp/easybox/$(VIRTUAL_ID)  目錄下,映射就已經完成了。再將該文件添加至工程中(不參與編譯,引用路徑須是  /tmp/easybox/$(VIRTUAL_ID)/*  ),此時即可以愉快地玩(tiao)耍(shi)了。源碼映射原理示意圖:

咱們能夠藉助  dwarfdump  命令查看二進制中相關的Debug信息。修改先後的對比圖:

其餘方案 

翻閱LLVM文檔能夠查到另一些關於源碼映射的資料:

1. 在運行階段,能夠藉助lldb的source-map來修改源文件所處的位置,顯然使用起來很不方便。

2. 動態連接庫能夠經過配置plist文件來完成源碼映射,但在實際應用中,動態庫會嚴重影響啓動速度和包體積,一般各個組件均以靜態庫存在,故也未採用此方案。

二進制化以後,編譯速度有了質的飛躍,在實踐過程當中還針對一些細節的進行優化。

3.2 Clang模塊緩存(頭文件檢索緩存)

百度App現行有近三百個組件,組件間的依賴關係很是複雜,組件規模大小不一,組件間進行調用時在預處理階段不一樣文件反覆的import致使的頭文件檢索過程十分耗時,如蘋果推薦,咱們將大多數組件編譯爲framework,而在非framework的狀況下,能夠經過生成modulemap來完成static library的Clang Module Cache,具體可參見 WWDC 18: Behind the Scenes of the Xcode Build Process,這裏就再也不贅述。

須要注意的是,Clang Module Cache並不會隨着Xcode Clean而被清除,以前偶爾會出現由該Cache引發的一些奇怪的編不過的bug,此時則須要將DerivedData下的ModuleCache.noindex文件夾清除便可,不過隨着後來Xcode的更新,這一問題獲得了緩解。

3.3 資源編譯緩存

爲了優化包體積,百度App工程大部分圖片資源採用的是xcassets,在打包App過程當中,須要經過actool將所有的xcassets編譯合併進一個car文件,actool在處理時並無作緩存,因爲圖片資源較多,每次編譯xcassets都耗時近一分鐘(13' RMBP)。因爲源碼增量編譯自己就比較快,那這一分鐘對於編譯速度實際的影響是很是大的。解決辦法就是經過rsync備份一份資源來檢驗xcassets是否發生過變化,並只在資源或條件發生變化再從新觸發編譯。

理念

在實踐EasyBox的過程當中,咱們逐步抽象確立了一些理念,這些理念在咱們工程化的過程當中起到了重要做用。

丨法治優先

項目通過長期的沉澱,每每會造成各類規範,團隊規模大、規範多,再也不是羣裏喊一嗓子就能夠解決的事情,應該儘量地經過工具來實行強制約束來確保規範被執行。規範也應當儘量的簡單易記,並經過工具輔助將遵照成本降到最低。

丨限制管理(編譯隔離)

組件邊界要明確,組件的依賴關係、API接口等要可控,這將有助於組件在不一樣環境(App)下編譯構建、多端複用,使得輸出時開發者對於組件的依賴是有預期的。反之,當組件邊界模糊時,真正輸出組件時,會暴露諸多問題,很容易形成拔出蘿蔔帶出泥的窘境。

丨問題前置

工具可以暴露的問題,應當儘量早的暴露出來,譬如能在提代碼以前暴露的就不要延遲到持續集成暴露,問題發現的越早,修復成本就越低,這有助於避免開發者在實際開發過程當中反覆返工。

對比業界

業內比較普遍採用的是將CocoaPods做爲工具鏈,其對於小工程的實踐是很是經典的。可是對大工程的支持力度是相對較弱的,其編譯隔離與去工程化是相互矛盾的(最近發佈的1.7 beta開始支持獨立工程徹底隔離了,但還處於很是初期的階段),不支持層級劃分,對於多倉庫、二進制管理也都是不無小補,而這些幾乎是大工程實踐必備的工程能力。

在技術方案上,EasyBox設計之初的理念就與CocoaPods有着很大的區別,因此並無採用業界通用作法基於CocoaPods改造,而是直接基於xcodeproj從新實現了一套全新的工具鏈。功能對比圖:

將來

EasyBox自上線以來自身也在不斷地迭代進化,目前還存在一些地方不夠完善,後續會將已經適配其餘包管理器的三方開源庫無縫兼容。目前內部已在百度App、西番、看多多等多個團隊獲得應用,後續還會推廣至更多團隊。同時,咱們後續也將推進開源工做,將整套解決方案打包開源出去。

結語

近年來,國內對超級App的追逐,致使客戶端項目極速膨脹,而由此帶來的工程化問題變得尤其嚴重。工欲善其事,必先利其器。大型團隊向研發工具鏈方向的投入每每是用戶所看不到的,可是對開發者來講收益倒是很是明顯的。組件規範統一,維護、換手成本相對較低;完善的物理隔離與編譯隔離將組件邊界劃分清晰,物各有主;層級的引入使得架構層級更加明晰,上手成本更低;在不影響開發人員調試成本的狀況下,將編譯速度壓至2分鐘之內(提高90%),也大幅提高了App出包速度。

咱們以前將百度App(iOS端)原有零散的研發工具推倒重來,從零搭建了這套現代、高效、優雅的研發工具鏈。工具鏈對於組件多產品複用、中臺化建設也起着相當重要的支撐做用。但工具並不能解決代碼問題,得益於組件化工做的推動(後續將會有專文介紹),使得百度App這頭大象很是順利地在短期內就穿上了這套裝甲,讓這隻步履蹣跚的大象也能夠擁有矯健靈活的舞步。

EasyBox更多承擔的是開發環境的配置,配合背後強大的流程扭轉中樞,將開發與後續的提測、發佈、准入等工做流造成標準化研發閉環,組成集管理、迭代、輸出、集成等功能於一體的一站式研發中臺,感興趣的同窗敬請期待後續文章。

參考資料

  1.  https://semver.org/
  2. https://llvm.org/docs/SourceLevelDebugging.html
  3. https://lldb.llvm.org/use/symbols.html
  4. https://github.com/apple/swift-package-manager/blob/master/Documentation/Internals/PackageManagerCommunityProposal.md
  5. https://developer.apple.com/videos/play/wwdc2018/415/
  6. https://cocoapods.org/
  7. https://gerrit.googlesource.com/git-repo/
  8. https://gerrit-review.googlesource.com/Documentation/intro-user.html#topics

---------------------------------

在微信-搜索頁面中輸入「百度App技術」,便可關注微信官方帳號;

原文連接地址:https://developer.baidu.com/topic/show/290273

相關文章
相關標籤/搜索