前言介紹
2016.09.26他去,抖音版本 1.0.0 上線毙驯,隨后不斷迭代優(yōu)化和豐富產(chǎn)品,截止目前灾测,抖音日活躍用戶突破 6 億爆价,短短 4 年間,抖音從零爆發(fā)性增長(zhǎng)媳搪。
快速的業(yè)務(wù)發(fā)展也對(duì)技術(shù)支撐提出了更高的要求铭段,為了保障敏捷的業(yè)務(wù)開(kāi)發(fā),提升跨團(tuán)隊(duì)的協(xié)同合作效率秦爆,提高本地研發(fā)和 CI/CD 效率序愚,抖音 iOS App 工程架構(gòu)在不同的階段進(jìn)行了不同的技術(shù)方案的改進(jìn),滿足合理的架構(gòu)演化等限,同時(shí)又不影響正常的業(yè)務(wù)迭代速度爸吮。
抖音工程架構(gòu)演進(jìn)
架構(gòu)演進(jìn)的本質(zhì)是為了提高研發(fā)效率芬膝,提高代碼穩(wěn)定性和保證代碼質(zhì)量。架構(gòu)要解決的問(wèn)題是如何組織代碼形娇。
合理的架構(gòu)設(shè)計(jì)可以解決大型項(xiàng)目跨團(tuán)隊(duì)協(xié)作分工和多業(yè)務(wù)線并行開(kāi)發(fā)的效率問(wèn)題锰霜。抖音工程代碼從一開(kāi)始就采用了組件化思路,依賴管理工具是定制版的 Cocoapods桐早。
以下動(dòng)畫介紹了抖音工程架構(gòu)經(jīng)歷的四個(gè)階段的演進(jìn)過(guò)程:
圖1:抖音項(xiàng)目工程架構(gòu)演進(jìn)
組件化
在大型項(xiàng)目快速發(fā)展的過(guò)程中癣缅,要保證敏捷開(kāi)發(fā)迭代的最大障礙就是快速膨脹的代碼體積導(dǎo)致的編譯效率問(wèn)題,依賴關(guān)系復(fù)雜化問(wèn)題勘畔,以及業(yè)務(wù)線代碼沖突問(wèn)題所灸。
移動(dòng)端項(xiàng)目可以類比后端項(xiàng)目中采用的微服務(wù)架構(gòu),要解決多業(yè)務(wù)線并行開(kāi)發(fā)炫七、并行測(cè)試問(wèn)題爬立,采用流水線式迭代開(kāi)發(fā),提高發(fā)版万哪、集成侠驯、交付、提審奕巍、發(fā)布效率吟策,結(jié)合分治思想技術(shù)選型上可以采用組件化的方案。
大部分小型項(xiàng)目的止,組件化僅僅做到代碼分倉(cāng)檩坚,使用 Cocoapods 的來(lái)管理組件依賴,就像抖音項(xiàng)目最初的工程形態(tài)诅福。
但是對(duì)于幾百號(hào)人匾委、幾十個(gè)業(yè)務(wù)線規(guī)模的大型項(xiàng)目,需要設(shè)計(jì)一套合理的組件分層架構(gòu)氓润,理清組件間依賴關(guān)系赂乐,需要 CI/CD 工具鏈支撐組件發(fā)版與集成,需要本地研發(fā)工具支撐本地代碼同步咖气、工程配置挨措、依賴管理和效率優(yōu)化。
流水線式迭代開(kāi)發(fā)
流水線(pipeline)技術(shù)是指在程序執(zhí)行時(shí)多條指令重疊進(jìn)行操作的一種準(zhǔn)并行實(shí)現(xiàn)技術(shù)崩溪,該技術(shù)可以充分提高資源的利用率浅役,同時(shí)縮短產(chǎn)品的研發(fā)周期。 對(duì)于客戶端項(xiàng)目悯舟,流水線技術(shù)能很大程度滿足敏捷開(kāi)發(fā)迭代的節(jié)奏担租。
圖2:抖音流水線式迭代發(fā)版
抖音工程架構(gòu)演進(jìn)
階段一:抖音原始工程架構(gòu)(Original architecture of project)
圖3:抖音項(xiàng)目原始工程架構(gòu)圖
抖音項(xiàng)目一開(kāi)始是單體架構(gòu)+Cocoapods,業(yè)務(wù)代碼抵怎、工程配置奋救、資源文件全部放在一個(gè)大業(yè)務(wù)倉(cāng)庫(kù)岭参。由 Podfile 文件描述第三方倉(cāng)庫(kù)的依賴版本。
圖4:抖音項(xiàng)目原始工程架目錄結(jié)構(gòu)
階段二:分離殼工程后的工程架構(gòu)(After splitting of host shell pod)
圖5:拆分殼工程后的工程架構(gòu)
分離殼工程后尝艘,工程配置演侯、部分系統(tǒng)資源、工程主入口被拆分到主宿主殼工程背亥。
Podfile 拆分出版本依賴管理文件 Podfile.seer秒际,由依賴管理平臺(tái)進(jìn)行各個(gè)版本的容器化管理,業(yè)務(wù)倉(cāng)跟隨宿主集成發(fā)版狡汉,打平依賴娄徊,解決版本依賴決議耗時(shí)問(wèn)題。
大業(yè)務(wù)倉(cāng)中的代碼和資源被拆分到各個(gè)業(yè)務(wù)線的倉(cāng)庫(kù)下盾戴,由 podspec 文件描述內(nèi)外依賴寄锐。業(yè)務(wù)線倉(cāng)庫(kù)增加 ModuleInterface subspec,存放對(duì)外接口尖啡,采用依賴注入方式實(shí)現(xiàn)接口隔離橄仆,初步建立接口層。
業(yè)務(wù)倉(cāng)庫(kù)之間規(guī)定只能依賴其他業(yè)務(wù)倉(cāng)庫(kù)的 ModuleInterface subspec衅斩,通過(guò) lint 進(jìn)行編譯檢查盆顾。
部分基礎(chǔ)能力代碼被拆分成基礎(chǔ)倉(cāng)庫(kù),跟第三方倉(cāng)庫(kù)一樣獨(dú)立發(fā)版畏梆。本地研發(fā)工具支持單倉(cāng)開(kāi)發(fā)和多倉(cāng)開(kāi)發(fā)您宪,不參與代碼修改的倉(cāng)庫(kù)通過(guò)二進(jìn)制的方式進(jìn)行鏈接。同時(shí) CI 流程上也支持通過(guò)二進(jìn)制打測(cè)試包奠涌,提高打包效率蚕涤。
圖6:抖音項(xiàng)目拆分殼工程后目錄結(jié)構(gòu)
殼工程
圖7:殼工程抽象
為了滿足一個(gè)工程同時(shí)支持多個(gè)項(xiàng)目、部分業(yè)務(wù)線功能復(fù)用铣猩、部分業(yè)務(wù)線中臺(tái)化發(fā)展的需求,我們把所有業(yè)務(wù)線抽象成獨(dú)立的 Pod茴丰,所有業(yè)務(wù) Pod 必須通過(guò)宿主的殼工程進(jìn)行集成發(fā)版达皿。
殼工程包含了項(xiàng)目依賴的 Pod 信息描述,同時(shí)還包括工程的配置贿肩、部分系統(tǒng)級(jí)別的資源文件峦椰、工程主入口代碼√妫基于多份宿主殼工程汤功,一份代碼可以打包出抖音、抖音極速版等項(xiàng)目溜哮。
同時(shí)滔金,基于宿主殼工程色解,一些業(yè)務(wù)線可以通過(guò)自動(dòng)化同步生成自己的子殼工程,實(shí)現(xiàn)業(yè)務(wù)線自己的 Example 工程餐茵,進(jìn)行獨(dú)立開(kāi)發(fā)科阎,比如有語(yǔ)音通話的 Example 工程,有工具的 Example 工程忿族,有直播的 Example 工程等等锣笨。
參考:iOS組件化
圖8:子殼工程配置同步同步
接口層
接口層顧名思義,只提供依賴的抽象接口道批,所有接口都是 protocol 協(xié)議聲明错英。
接口層限制了所有其他依賴,類隆豹、枚舉椭岩、 外部協(xié)議都采用前向聲明,podspec 上只允許聲明對(duì) DI(依賴注入)框架的依賴噪伊。接口層滿足封裝簿煌、隔離和組合的原則。
業(yè)務(wù)層面對(duì)外封裝了實(shí)現(xiàn)代碼鉴吹;
編譯層面隔離了組件間依賴傳遞姨伟,減少頭文件 import 嵌套提高編譯緩存的命中率,對(duì)于 swift 業(yè)務(wù)組件豆励,還能達(dá)到減少編譯傳遞的問(wèn)題夺荒;
架構(gòu)層面聲明抽象協(xié)議支持接口組合;
DI 容器框架同時(shí)支持 stateless DI 容器良蒸,也支持 stateful DI 容器技扼。
依賴打平
采用 Cocoapods 本身自帶的版本依賴決議進(jìn)行版本分析會(huì)消耗大量的時(shí)間;
Podfile.lock 過(guò)于繁瑣嫩痰,可讀性很差剿吻,難以解決 Podfile.lock 的沖突;
隱式依賴被動(dòng)/不符合預(yù)期地升級(jí)串纺,難以確定性地聲明所有依賴丽旅,防止隱式依賴被升級(jí);
依賴版本在 Podfile/Podfile.lock 重復(fù)聲明纺棺,增加了解決沖突的成本榄笙;
Podfile.lock 參與依賴版本決議流程比較復(fù)雜,會(huì)出現(xiàn)不符合預(yù)期的情況祷蝌。
圖9:把版本管理和倉(cāng)庫(kù)源信息遷移到 Podfile.seer 文件
hook 掉 Cocoapods 采用 podfile.lock 進(jìn)行版本決議的邏輯茅撞,采用 Podfile.seer 文件直接描述所有組件的版本信息,打平依賴。
階段三:?jiǎn)蝹}(cāng)多組件工程架構(gòu)(Multicomponents in single repo)
圖10:拆分單倉(cāng)多組件后的工程架構(gòu)
采用單倉(cāng)多組件后米丘,每個(gè)業(yè)務(wù)線倉(cāng)庫(kù)支持添加 podspec 增加組件剑令,實(shí)現(xiàn)更小粒度的二進(jìn)制依賴。業(yè)務(wù)線倉(cāng)庫(kù)內(nèi)劃分業(yè)務(wù)實(shí)現(xiàn)層蠕蚜、業(yè)務(wù)接口層尚洽、服務(wù)層和基礎(chǔ)層,都是通過(guò)集成方式發(fā)版靶累。
新增的服務(wù)層主要存放公共的業(yè)務(wù)邏輯和通用服務(wù)腺毫,限制 UI,一是滿足業(yè)務(wù)邏輯復(fù)用挣柬,二是滿足子殼工程最小化二進(jìn)制依賴潮酒。同時(shí)服務(wù)層的服務(wù)接口也達(dá)到隔離依賴傳遞的目的,在不同的宿主上邪蛔,支持通過(guò)改變服務(wù)層實(shí)現(xiàn)替換后臺(tái)能力或者底層能力急黎。建立分層間的依賴準(zhǔn)入規(guī)則,完善 lint 編譯鏈接檢查侧到。
圖11:?jiǎn)蝹}(cāng)多組件目錄結(jié)構(gòu)
編譯鏈接完備性校驗(yàn)
編譯校驗(yàn):分開(kāi)編譯各個(gè) subspec勃教,確保每個(gè) subspec 的依賴是正確的(由于 subspec 沒(méi)有編譯隔離)
接口符號(hào)校驗(yàn):校驗(yàn)當(dāng)前接口組件(ModuleInterface)中符號(hào)是否完備的,以保證其他組件單獨(dú)引用是否能正常使用匠抗。如 extern 聲明的全局變量故源。
分層依賴準(zhǔn)入規(guī)則:
高層依賴低層
實(shí)現(xiàn)依賴接口
接口層無(wú)依賴
前向聲明優(yōu)先
服務(wù)層去"UI"
以下動(dòng)畫展示了業(yè)務(wù)實(shí)現(xiàn)層和服務(wù)實(shí)現(xiàn)允許依賴的分層:
參考:iOS架構(gòu)
圖12:組件依賴關(guān)系示意圖動(dòng)畫
階段四:Example 子殼工程架構(gòu)(Subshell for bizcomponent in example project)
圖13:子殼工程架構(gòu)
每個(gè)業(yè)務(wù)倉(cāng)從宿主同步工程配置構(gòu)建子殼工程。增加 AWELaunchKit 為子殼工程提供運(yùn)行時(shí)的基礎(chǔ)能力汞贸。通過(guò)服務(wù)層提供業(yè)務(wù)間運(yùn)行時(shí)共享的服務(wù)能力绳军,滿足代碼復(fù)用和更小二進(jìn)制依賴。
圖14:子殼工程目錄結(jié)構(gòu)
AWELaunchKit
AWELaunchKit 框架為宿主和其他子殼工程提供了基礎(chǔ)服務(wù)的依賴和初始化配置矢腻。同時(shí)提供了一套啟動(dòng)加載的 BootTasks 管理框架门驾,部分業(yè)務(wù)涉及啟動(dòng)相關(guān)的邏輯可以在業(yè)務(wù)倉(cāng)對(duì)應(yīng)的服務(wù)層中實(shí)現(xiàn),并通過(guò) BootTasks 管理框架注冊(cè)到啟動(dòng)加載器里面多柑。
同時(shí)框架還提供了一套宿主 UI 入口和自定義入口框架奶是。為了方便測(cè)試和調(diào)試,也整合了整套測(cè)試調(diào)試框架竣灌。
圖15:子殼工程依賴關(guān)系
組件化探索過(guò)程中遇到的一些問(wèn)題:
二進(jìn)制污染
組件之間的依賴除了顯式的依賴诫隅,還存在很多隱式依賴,代碼層面帐偎,除了普通的接口依賴,還有宏依賴蛔屹、枚舉依賴削樊、全局變量依賴以及內(nèi)聯(lián)函數(shù)等的依賴。單倉(cāng) lint 進(jìn)行編譯鏈接完備性檢查并不能解決依賴變動(dòng)對(duì)其他二進(jìn)制的影響。
因此需要借助源碼層面的依賴分析漫贞,判斷當(dāng)前組件的變更對(duì)其他依賴當(dāng)前組件的二進(jìn)制是否有影響甸箱,在 CI 流程中及時(shí)發(fā)現(xiàn)并攔截。否則錯(cuò)誤的二進(jìn)制發(fā)版迅脐,會(huì)直接導(dǎo)致整個(gè) CI 研發(fā)流程和本地研發(fā)都受到影響芍殖。
編譯優(yōu)化
編譯優(yōu)化最高效的方式就是提高緩存的利用率。對(duì)于本地研發(fā)和 CI 流程谴蔑,都涉及分布式編譯緩存同步豌骏。同時(shí)通過(guò)編譯參數(shù)優(yōu)化、依賴優(yōu)化隐锭、hmap 優(yōu)化也能不同程度的提高編譯效率
主干分支穩(wěn)定性問(wèn)題
對(duì)于多業(yè)務(wù)線并行開(kāi)發(fā)窃躲,幾百號(hào)人的業(yè)務(wù)開(kāi)發(fā)團(tuán)隊(duì),如果主干分支一旦出現(xiàn)問(wèn)題钦睡,那么解決問(wèn)題的時(shí)間就需要乘上幾百倍蒂窒。因此,需要從編譯層面和運(yùn)行層面都要有足夠的機(jī)制去保證一個(gè)穩(wěn)定的主干分支荞怒,才能保證業(yè)務(wù)側(cè)的長(zhǎng)期穩(wěn)定性洒琢。
業(yè)務(wù)層的依賴耦合問(wèn)題
大型項(xiàng)目動(dòng)則千萬(wàn)行的代碼,代碼間的依賴關(guān)系是復(fù)雜的網(wǎng)狀關(guān)系褐桌。需要基于代碼的語(yǔ)法樹(shù)模型衰抑,從語(yǔ)義中去分析不合理的依賴,并輸出治理的方案撩嚼。
我們內(nèi)部自研了源碼依賴關(guān)系分析平臺(tái)用于依賴關(guān)系分析監(jiān)控和代碼治理停士,長(zhǎng)期監(jiān)控組件間的依賴度。同時(shí)完丽,需要建立依賴健康度模型恋技,從長(zhǎng)期演進(jìn)的角度去監(jiān)控防止代碼的劣化。
圖16:spider 組件依賴分析平臺(tái)
總結(jié)
大型項(xiàng)目的組件化工作是一個(gè)系統(tǒng)性工程逻族。涉及工程架構(gòu)的改造蜻底、CI/CD 研發(fā)工具鏈的支撐、本地研發(fā)工具鏈的支撐聘鳞,業(yè)務(wù)架構(gòu)的設(shè)計(jì)優(yōu)化薄辅,需要從各個(gè)方面綜合考慮成本和收益。
沒(méi)有最好的架構(gòu)抠璃,只有更好的架構(gòu)站楚,在架構(gòu)演進(jìn)的過(guò)程中,我們需要充分考慮架構(gòu)的改動(dòng)對(duì)業(yè)務(wù)的影響以及能給業(yè)務(wù)帶來(lái)的收益搏嗡。好的架構(gòu)一定是能幫助業(yè)務(wù)節(jié)省時(shí)間窿春,保證質(zhì)量的拉一。與此同時(shí),我們?cè)诩軜?gòu)改進(jìn)的過(guò)程中旧乞,要保證不能影響業(yè)務(wù)的正常迭代蔚润,所以向前兼容且避免大面積沖突也是很重要的事情。
組件化里面處處都有驚喜尺栖,比如一個(gè)小小的 hmap 優(yōu)化嫡纠,可以很大程度的減少編譯耗時(shí),比如一個(gè)二進(jìn)制的壓縮和解壓的優(yōu)化延赌,可以很大程度減少 pod install 的整體耗時(shí)除盏。
當(dāng)然這里面也會(huì)有很多很棘手的問(wèn)題,需要通過(guò)一些特殊的方案解決皮胡,比如針對(duì)分布式開(kāi)發(fā)痴颊,由于阻塞式發(fā)版必然會(huì)導(dǎo)致一些不同分支存在沖突的代碼發(fā)版后影響主干的穩(wěn)定性。
由于文章篇幅有限屡贺,只能點(diǎn)到即止地介紹當(dāng)前一些工作成果和思考蠢棱,各個(gè) Topic 還有一些新的方向在探索,如果你對(duì) iOS 底層原理甩栈、架構(gòu)設(shè)計(jì)泻仙、構(gòu)建系統(tǒng)、如何面試有興趣了解量没,你也可以私信我及時(shí)獲取最新資料以及面試相關(guān)資料玉转。如果你有什么意見(jiàn)和建議歡迎給我留言。
參考:iOS底層面試題
參考:字節(jié)跳動(dòng)技術(shù)團(tuán)隊(duì)