抖音 iOS 工程架構演進
前言介紹
2016.09.26,抖音版本 1.0.0 上線,隨後不斷迭代優化和豐富產品,截止目前,抖音日活躍用戶突破 6 億,短短 4 年間,抖音從零爆發性增長。
快速的業務發展也對技術支撐提出了更高的要求,爲了保障敏捷的業務開發,提升跨團隊的協同合作效率,提高本地研發和 CI/CD 效率,抖音 iOS App 工程架構在不同的階段進行了不同的技術方案的改進,滿足合理的架構演化,同時又不影響正常的業務迭代速度。
抖音工程架構演進
架構演進的本質是爲了提高研發效率,提高代碼穩定性和保證代碼質量。架構要解決的問題是如何組織代碼。
合理的架構設計可以解決大型項目跨團隊協作分工和多業務線並行開發的效率問題。抖音工程代碼從一開始就採用了組件化思路,依賴管理工具是定製版的 Cocoapods。
以下動畫介紹了抖音工程架構經歷的四個階段的演進過程:
圖 1:抖音項目工程架構演進
組件化
在大型項目快速發展的過程中,要保證敏捷開發迭代的最大障礙就是快速膨脹的代碼體積導致的編譯效率問題,依賴關係複雜化問題,以及業務線代碼衝突問題。
移動端項目可以類比後端項目中採用的微服務架構,要解決多業務線並行開發、並行測試問題,採用流水線式迭代開發,提高發版、集成、交付、提審、發佈效率,結合分治思想技術選型上可以採用組件化的方案。
大部分小型項目,組件化僅僅做到代碼分倉,使用 Cocoapods 的來管理組件依賴,就像抖音項目最初的工程形態。
但是對於幾百號人、幾十個業務線規模的大型項目,需要設計一套合理的組件分層架構,理清組件間依賴關係,需要 CI/CD 工具鏈支撐組件發版與集成,需要本地研發工具支撐本地代碼同步、工程配置、依賴管理和效率優化。
流水線式迭代開發
流水線(pipeline)技術是指在程序執行時多條指令重疊進行操作的一種準並行實現技術,該技術可以充分提高資源的利用率,同時縮短產品的研發週期。對於客戶端項目,流水線技術能很大程度滿足敏捷開發迭代的節奏。
圖 2:抖音流水線式迭代發版
抖音工程架構演進
階段一:抖音原始工程架構(Original architecture of project)
圖 3:抖音項目原始工程架構圖
抖音項目一開始是單體架構 + Cocoapods,業務代碼、工程配置、資源文件全部放在一個大業務倉庫。由 Podfile 文件描述第三方倉庫的依賴版本。
圖 4:抖音項目原始工程架目錄結構
階段二:分離殼工程後的工程架構(After splitting of host shell pod)
圖 5:拆分殼工程後的工程架構
分離殼工程後,工程配置、部分系統資源、工程主入口被拆分到主宿主殼工程。
Podfile 拆分出版本依賴管理文件 Podfile.seer,由依賴管理平臺進行各個版本的容器化管理,業務倉跟隨宿主集成發版,打平依賴,解決版本依賴決議耗時問題。
大業務倉中的代碼和資源被拆分到各個業務線的倉庫下,由 podspec 文件描述內外依賴。業務線倉庫增加 ModuleInterface subspec,存放對外接口,採用依賴注入方式實現接口隔離,初步建立接口層。
業務倉庫之間規定只能依賴其他業務倉庫的 ModuleInterface subspec,通過 lint 進行編譯檢查。
部分基礎能力代碼被拆分成基礎倉庫,跟第三方倉庫一樣獨立發版。本地研發工具支持單倉開發和多倉開發,不參與代碼修改的倉庫通過二進制的方式進行鏈接。同時 CI 流程上也支持通過二進制打測試包,提高打包效率。
圖 6:抖音項目拆分殼工程後目錄結構
殼工程
圖 7:殼工程抽象
爲了滿足一個工程同時支持多個項目、部分業務線功能複用、部分業務線中臺化發展的需求,我們把所有業務線抽象成獨立的 Pod,所有業務 Pod 必須通過宿主的殼工程進行集成發版。
殼工程包含了項目依賴的 Pod 信息描述,同時還包括工程的配置、部分系統級別的資源文件、工程主入口代碼。基於多份宿主殼工程,一份代碼可以打包出抖音、抖音極速版等項目。
同時,基於宿主殼工程,一些業務線可以通過自動化同步生成自己的子殼工程,實現業務線自己的 Example 工程,進行獨立開發,比如有語音通話的 Example 工程,有工具的 Example 工程,有直播的 Example 工程等等。
圖 8:子殼工程配置同步同步
接口層
接口層顧名思義,只提供依賴的抽象接口,所有接口都是 protocol 協議聲明。
接口層限制了所有其他依賴,類、枚舉、 外部協議都採用前向聲明,podspec 上只允許聲明對 DI(依賴注入)框架的依賴。接口層滿足封裝、隔離和組合的原則。
-
業務層面對外封裝了實現代碼;
-
編譯層面隔離了組件間依賴傳遞,減少頭文件 import 嵌套提高編譯緩存的命中率,對於 swift 業務組件,還能達到減少編譯傳遞的問題;
-
架構層面聲明抽象協議支持接口組合;
-
DI 容器框架同時支持 stateless DI 容器,也支持 stateful DI 容器。
依賴打平
-
採用 Cocoapods 本身自帶的版本依賴決議進行版本分析會消耗大量的時間;
-
Podfile.lock 過於繁瑣,可讀性很差,難以解決 Podfile.lock 的衝突;
-
隱式依賴被動 / 不符合預期地升級,難以確定性地聲明所有依賴,防止隱式依賴被升級;
-
依賴版本在 Podfile/Podfile.lock 重複聲明,增加了解決衝突的成本;
-
Podfile.lock 參與依賴版本決議流程比較複雜,會出現不符合預期的情況。
圖 9:把版本管理和倉庫源信息遷移到 Podfile.seer 文件
- hook 掉 Cocoapods 採用 podfile.lock 進行版本決議的邏輯,採用 Podfile.seer 文件直接描述所有組件的版本信息,打平依賴。
階段三:單倉多組件工程架構(Multicomponents in single repo)
圖 10:拆分單倉多組件後的工程架構
採用單倉多組件後,每個業務線倉庫支持添加 podspec 增加組件,實現更小粒度的二進制依賴。業務線倉庫內劃分業務實現層、業務接口層、服務層和基礎層,都是通過集成方式發版。
新增的服務層主要存放公共的業務邏輯和通用服務,限制 UI,一是滿足業務邏輯複用,二是滿足子殼工程最小化二進制依賴。同時服務層的服務接口也達到隔離依賴傳遞的目的,在不同的宿主上,支持通過改變服務層實現替換後臺能力或者底層能力。建立分層間的依賴准入規則,完善 lint 編譯鏈接檢查。
圖 11:單倉多組件目錄結構
編譯鏈接完備性校驗
-
編譯校驗:分開編譯各個 subspec,確保每個 subspec 的依賴是正確的 (由於 subspec 沒有編譯隔離)
-
接口符號校驗:校驗當前接口組件 (ModuleInterface) 中符號是否完備的,以保證其他組件單獨引用是否能正常使用。如 extern 聲明的全局變量。
分層依賴准入規則:
-
高層依賴低層
-
實現依賴接口
-
接口層無依賴
-
前向聲明優先
-
服務層去 "UI"
以下動畫展示了業務實現層和服務實現允許依賴的分層:
圖 12:組件依賴關係示意圖動畫
階段四:Example 子殼工程架構(Subshell for bizcomponent in example project)
圖 13: 子殼工程架構
每個業務倉從宿主同步工程配置構建子殼工程。增加 AWELaunchKit 爲子殼工程提供運行時的基礎能力。通過服務層提供業務間運行時共享的服務能力,滿足代碼複用和更小二進制依賴。
圖 14:子殼工程目錄結構
AWELaunchKit
AWELaunchKit 框架爲宿主和其他子殼工程提供了基礎服務的依賴和初始化配置。同時提供了一套啓動加載的 BootTasks 管理框架,部分業務涉及啓動相關的邏輯可以在業務倉對應的服務層中實現,並通過 BootTasks 管理框架註冊到啓動加載器裏面。
同時框架還提供了一套宿主 UI 入口和自定義入口框架。爲了方便測試和調試,也整合了整套測試調試框架。
圖 15:子殼工程依賴關係
組件化探索過程中遇到的一些問題:
二進制污染
組件之間的依賴除了顯式的依賴,還存在很多隱式依賴,代碼層面,除了普通的接口依賴,還有宏依賴、枚舉依賴、全局變量依賴以及內聯函數等的依賴。單倉 lint 進行編譯鏈接完備性檢查並不能解決依賴變動對其他二進制的影響。
因此需要藉助源碼層面的依賴分析,判斷當前組件的變更對其他依賴當前組件的二進制是否有影響,在 CI 流程中及時發現並攔截。否則錯誤的二進制發版,會直接導致整個 CI 研發流程和本地研發都受到影響。
編譯優化
編譯優化最高效的方式就是提高緩存的利用率。對於本地研發和 CI 流程,都涉及分佈式編譯緩存同步。同時通過編譯參數優化、依賴優化、hmap 優化也能不同程度的提高編譯效率
主幹分支穩定性問題
對於多業務線並行開發,幾百號人的業務開發團隊,如果主幹分支一旦出現問題,那麼解決問題的時間就需要乘上幾百倍。因此,需要從編譯層面和運行層面都要有足夠的機制去保證一個穩定的主幹分支,才能保證業務側的長期穩定性。
業務層的依賴耦合問題
大型項目動則千萬行的代碼,代碼間的依賴關係是複雜的網狀關係。需要基於代碼的語法樹模型,從語義中去分析不合理的依賴,並輸出治理的方案。
我們內部自研了源碼依賴關係分析平臺用於依賴關係分析監控和代碼治理,長期監控組件間的依賴度。同時,需要建立依賴健康度模型,從長期演進的角度去監控防止代碼的劣化。
圖 16:spider 組件依賴分析平臺
總結
大型項目的組件化工作是一個系統性工程。涉及工程架構的改造、CI/CD 研發工具鏈的支撐、本地研發工具鏈的支撐,業務架構的設計優化,需要從各個方面綜合考慮成本和收益。
沒有最好的架構,只有更好的架構,在架構演進的過程中,我們需要充分考慮架構的改動對業務的影響以及能給業務帶來的收益。好的架構一定是能幫助業務節省時間,保證質量的。與此同時,我們在架構改進的過程中,要保證不能影響業務的正常迭代,所以向前兼容且避免大面積衝突也是很重要的事情。
組件化裏面處處都有驚喜,比如一個小小的 hmap 優化,可以很大程度的減少編譯耗時,比如一個二進制的壓縮和解壓的優化,可以很大程度減少 pod install 的整體耗時。
當然這裏面也會有很多很棘手的問題,需要通過一些特殊的方案解決,比如針對分佈式開發,由於阻塞式發版必然會導致一些不同分支存在衝突的代碼發版後影響主幹的穩定性。
由於文章篇幅有限,只能點到即止地介紹當前一些工作成果和思考,各個 Topic 還有一些新的方向在探索,如果你對 iOS 底層原理、架構設計、構建系統、自動化測試有深入瞭解,快來加入我們吧!
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/HHH5_IEbsR8iSmXSIdeutw