一、概述
經(jīng)過多年的發(fā)展,美柚iOS項(xiàng)目代碼已經(jīng)達(dá)到40W行+的規(guī)模,所使用的 Pod 庫(kù)的數(shù)量達(dá)到了110+,App Store 安裝包210M+,在這么大的項(xiàng)目規(guī)模下(CI機(jī)器 MAC配置:3 GHz 8-Core Intel Xeon E5;時(shí)間:發(fā)布20min+),(開發(fā)機(jī)器iMac :Retina 5K, 27-inch, 2017 融合硬盤;時(shí)間:build30min+)打包、編譯問題逐步成為我們團(tuán)隊(duì)一個(gè)躲不過的痛,嚴(yán)重影響了我們的研發(fā)效率與其他團(tuán)隊(duì)之間的協(xié)作。
我們一臺(tái)13年的ci機(jī)器同時(shí)需要承接七八個(gè)項(xiàng)目、多個(gè)分支的打包任務(wù),在有多個(gè)項(xiàng)目同時(shí)打包的情況,顯得尤其地力不從心。
在硬件資源有限的情況下,并且在無(wú)侵入
、無(wú)影響現(xiàn)有的業(yè)務(wù)
的前提下,如何解決這些擺在團(tuán)隊(duì)面前的難題,便成了我們迫在眉睫的迫切需求,最近半年多來(lái)一直在尋找加快打包速度的方案。
二、編譯提速探索與嘗試
1、CCache
CCache 是一個(gè)編譯緩存器,一個(gè)能夠把編譯的中間產(chǎn)物緩存起來(lái)的工具
其原理是通過把項(xiàng)目的源文件用ccache
編譯器編譯,然后緩存編譯生成的信息,從而在下一次編譯時(shí),利用這個(gè)緩存加快編譯的速度,目前支持的語(yǔ)言有:C
、C++
、Objective-C
、Objective-C++
下面這張圖基本就闡述了CCache的工作原理。
Ccache我們經(jīng)過在工程的一番嘗試、確實(shí)在某些方面上極大的提升了我們出包的速度。美柚iOS Ci打包從之前的最快20min+
出包到最快10min
,確實(shí)能夠給我們帶來(lái)比較不錯(cuò)的提升,大大加快了我們項(xiàng)目的出包速度。在我們項(xiàng)目運(yùn)行了幾個(gè)月后,對(duì)于我們項(xiàng)目的情況,也發(fā)現(xiàn)了一些問題,現(xiàn)在總結(jié)了以下幾點(diǎn):
優(yōu)點(diǎn):
- 滿足我們追求的
無(wú)侵入
、無(wú)影響現(xiàn)有的業(yè)務(wù)
的要求,無(wú)入侵、且開發(fā)人員無(wú)感知。 - 確實(shí)能大幅度地提升編譯速度,美柚項(xiàng)目上最快時(shí)提高3倍以上的編譯速度。
- 不需要對(duì)項(xiàng)目作出大調(diào)整,只需部署相關(guān)環(huán)境和一些腳本支持。
- 不需要改變開發(fā)工具鏈。
- 同一個(gè)目錄下,CCache 的緩存命中率相對(duì)穩(wěn)定。
對(duì)我們項(xiàng)目中有存在些問題點(diǎn):
- 在未有緩存的情況下,首次打包編譯的時(shí)間比原來(lái)的翻近一倍,原來(lái)20+min,首次將近40+min,在資源緊張的情況下,甚至是70min+。
- 修改一些引用較多的文件(如公共庫(kù)、底層庫(kù)改動(dòng)),容易造成大范圍的緩存失效,速度會(huì)變得比原來(lái)未使用ccache時(shí)更慢。
- 多個(gè)項(xiàng)目相同的組件不支持緩存共享,我們有多個(gè)分支打包的需求,修改目錄名稱后,緩存即失效。
- 我們機(jī)器的Ccache最大的緩存上限約18GB,且Debug/Release區(qū)別緩存,美柚iOS項(xiàng)目占用5GB+的緩存,多個(gè)項(xiàng)目、多個(gè)分支很容易超出上限,一臺(tái)Ci機(jī)器同時(shí)支持多個(gè)項(xiàng)目會(huì)觸發(fā)CCache清緩存。
- 對(duì)機(jī)器硬盤讀寫要求高,如不是全部固態(tài)硬盤,速度影響大。
- CCache 不支持 Clang Modules,系統(tǒng)框架例如 AVFoundation、CoreLocation等, Xcode 不會(huì)再幫你自動(dòng)引入,會(huì)導(dǎo)致編譯失敗。
- CCache 不支持 PCH 文件
- CCache 目前不支持 Swift
2、靜態(tài)庫(kù)二進(jìn)制方案的探索
雖然我們已經(jīng)在Ci的在應(yīng)用了Ccache已經(jīng)有提升近一倍的出包速度了,但是存在的問題也比較明顯。
在去年的某次技術(shù)周會(huì)上,我們的大佬提出了使用二進(jìn)制編譯的自研任務(wù),可以更進(jìn)一步提高研發(fā)效率。得到了大佬的啟發(fā)后,就一直在實(shí)踐與探索二進(jìn)制之路上。
我們的項(xiàng)目使用 CocoaPods 來(lái)管理第三方庫(kù)和私有庫(kù)的依賴,對(duì)大部分項(xiàng)目來(lái)說(shuō)應(yīng)該是標(biāo)配了。目前還是純 Objective-C 的項(xiàng)目,有少量C++,暫沒有引入 Swift。
3、 調(diào)研過的二進(jìn)制組件方案
下面列出研究過的一些主流方案以及最后沒有采用的原因,這些方案有各自的局限性,但是也給了我不少啟發(fā),思考過程跟最終方案一樣有價(jià)值。
3.1、Carthage
Carthage可以將一部分不常變的庫(kù)打包成framework,再引如到主工程,這樣可以減少開發(fā)過程中的編譯時(shí)間。Carthage 可以比較方便地調(diào)試源碼。因?yàn)槲覀兡壳耙呀?jīng)大規(guī)模使用 CocoaPods,轉(zhuǎn)用 Carthage 來(lái)做包管理需要做大量的轉(zhuǎn)換工作,變動(dòng)太大,不滿足我們的無(wú)侵入
、無(wú)影響現(xiàn)有的業(yè)務(wù)
,所以不考慮這個(gè)方案了。
3.2、cocoapods-packager
cocoapods-packager 可以將任意的 pod 打包成 Static Library,省去重復(fù)編譯的時(shí)間,一定程度上可以加快編譯時(shí)間,但是也有自身的問題:
- 優(yōu)化不徹底,只能優(yōu)化第三方和私有 Pod 的編譯速度,對(duì)于其他改動(dòng)頻繁的業(yè)務(wù)代碼無(wú)能為力
- 私有庫(kù)和第三方庫(kù)的后續(xù)更新很麻煩,當(dāng)有源碼修改后,需要重新打包上傳到內(nèi)部的 Git 倉(cāng)庫(kù)
- 過多的二進(jìn)制文件會(huì)拖慢 Git 的操作速度(目前還沒部署 Git 的 LFS)
- 難以調(diào)試源碼,不共享編譯緩存
- 打包成 Static Library 過程緩慢,需要通過pod lint,各個(gè)組件間又層層嵌套依賴,在現(xiàn)有階段來(lái)說(shuō),是難以實(shí)現(xiàn)的。
3.3、cocoapods-binary
Cocoapods-Binary(Cocoapods 官方推薦的二進(jìn)制插件), 是一個(gè)即時(shí)生成二進(jìn)制包并緩存,而非像 CocoaPods-Packager 僅僅針對(duì)單個(gè)私有庫(kù)的。原理是通過 CocoaPods 提供的 pre_install hook 在 pod install 的 prepare 階段攔截到當(dāng)前的 pod install context,進(jìn)而 fork 出一份獨(dú)立的 installer 以完成將預(yù)編譯源碼 clone 至 Pod/_Prebuild 目錄下,同時(shí)也存在幾個(gè)不足之處:
- 單私有源,無(wú)法實(shí)現(xiàn)服務(wù)端緩存,在沒有對(duì)應(yīng)二進(jìn)制包版本時(shí),pod install 后會(huì)額外去做二進(jìn)制包的生成,一定程度上會(huì)影響 pod install的速度。
- 開發(fā)者切回源碼調(diào)試,二進(jìn)制緩存會(huì)一并清空,需求重新編譯。
- 多個(gè)項(xiàng)目、不同分支的相同組件依舊無(wú)法共享
- 只支持framework,對(duì)我們項(xiàng)目現(xiàn)狀需要比較大的頭文件引用方式改動(dòng)。
3.4、cocoapods-bin 雙私有源
該插件進(jìn)行二進(jìn)制化的策略是采用雙私有源,即2個(gè)源地址,一個(gè)靜態(tài)服務(wù)器保存預(yù)先打好包的framework,一個(gè)是我們現(xiàn)在保存源碼的服務(wù)地址,在install的時(shí)候去選擇使用下載那個(gè),是個(gè)很不錯(cuò)的項(xiàng)目,深受啟發(fā)。
優(yōu)點(diǎn):
- 源碼和二進(jìn)制文件之間可以來(lái)回切換,速度比較快
- 不影響未接入二進(jìn)制化方案的業(yè)務(wù)團(tuán)隊(duì)
- 無(wú)二進(jìn)制版本時(shí),自動(dòng)采用源碼版本
- 接近原生 CocoaPods 的使用體驗(yàn)
?
對(duì)于在我們項(xiàng)目中存在的不足之處:
- 不支持指定分支,:podspec =>'', :git 方式的引用,對(duì)需要支持多個(gè)分支、多個(gè)業(yè)務(wù)線的項(xiàng)目是致命的。
- Archive二進(jìn)制文件時(shí),只能去spec倉(cāng)庫(kù)下載源碼,無(wú)法根據(jù)指定的分支去下載依賴庫(kù),導(dǎo)致編譯失敗、錯(cuò)亂的問題
- 依賴的組件需要推送到spec倉(cāng)庫(kù),很多私有庫(kù)并沒有推送到倉(cāng)庫(kù),且對(duì)于頻繁改動(dòng)的私有庫(kù),推送到倉(cāng)庫(kù)的verify很慢且與我們的開發(fā)習(xí)慣不符。
- 不支持.a靜態(tài)文件輸出,項(xiàng)目中大量類似
#import "IMYPulic.h"
需要一個(gè)個(gè)庫(kù)去編譯替換為#import <IMYPublic/IMYPublic. h>
,想想那110多個(gè)組件庫(kù)~ - 只支持一套環(huán)境,對(duì)于有Debug/Release/Dev開發(fā)環(huán)境需求的無(wú)法滿足
- 不支持二進(jìn)制組件的源碼調(diào)試
- 不能流暢的支持頻繁變動(dòng)的業(yè)務(wù)組件,操作會(huì)異常繁瑣。
- 針對(duì)于我們的項(xiàng)目,目前存在較大的障礙,無(wú)法使用起來(lái)。
4、 思考與總結(jié)
經(jīng)過一個(gè)多月來(lái)對(duì)業(yè)界存在的輪子的分析和思考,并在一定的實(shí)踐后,最后我們決定自己造一個(gè)靈活的、可配置的、簡(jiǎn)便的、無(wú)入侵的、雙私有源二進(jìn)制組件輔助插件。
接下來(lái)就擼起袖子,努力干吧~,騷年
三、雙私有源二進(jìn)制組件簡(jiǎn)介
在受到cocoapod-bin啟發(fā)后,在借鑒它的部分框架下,我們實(shí)現(xiàn)了自己的二進(jìn)制輔助插件cocoapods-imy-bin,并新增了幾個(gè)命令和二進(jìn)制源碼調(diào)試能力。
1、能做什么?只要能編譯通過,就制作
在cocoapods-imy-bin的輔助下,能無(wú)侵入式自動(dòng)化地制作所有符合條件的組件為二進(jìn)制,且對(duì)于頻繁的業(yè)務(wù)組件也能輕松的應(yīng)用上二進(jìn)制組件,無(wú)需多余操作,一切交給cocoapods-imy-bin自動(dòng)化運(yùn)行。
同時(shí)對(duì)于研發(fā)人員,也能提供獨(dú)立的二進(jìn)制組件給研發(fā)人員使用,解決日常的編譯 效率、跑真機(jī)效率低下,被墻等各種問題。
我們的口號(hào)是:
只要能編譯通過,就制作。
一次編譯到處使用,無(wú)入侵。
即使獨(dú)立的組件庫(kù)編譯不通過,整體項(xiàng)目能編譯通過也制作。
整套環(huán)境下來(lái),沒有讓我們的開發(fā)人員改變?cè)瓉?lái)的開發(fā)習(xí)慣,沒有改動(dòng)業(yè)務(wù)中相關(guān)的代碼,基本上做到了使用人員無(wú)感知狀態(tài)。
2、Ci打包效果
2.1 單項(xiàng)目 - 編譯最快2分鐘一次
上圖是個(gè)由我們打了幾千個(gè)包的經(jīng)驗(yàn)得出對(duì)單個(gè)項(xiàng)目編譯時(shí)間大致的曲線圖。這里假設(shè)一臺(tái)機(jī)器只一次只有一次job。Y軸編譯時(shí)間,X軸某次的編譯, 紅色線條表示的是原生(未使用Ccache和二進(jìn)制組件),黃色線表示使用了Ccache,藍(lán)色表示使用了二進(jìn)制組件。
由圖可以看出來(lái)在無(wú)任何輔助下原生的編譯時(shí)間曲線(紅色)是趨于平緩,在20min上下左右。Ccache和二進(jìn)制第一次在無(wú)任何緩存的情況下,在一定程度上是會(huì)比原生的耗時(shí),Ccache主要耗時(shí)在邊編譯邊緩存項(xiàng)目的編譯產(chǎn)物。二進(jìn)制主要耗時(shí)在編譯完成后,對(duì).a編譯產(chǎn)物的組裝和push到私有源倉(cāng)庫(kù)的時(shí)間上(這個(gè)跟所采用有關(guān)系,如果沒有利用Jenkins 編譯后的產(chǎn)物制作二進(jìn)制就不存在。)。
在ccache完全命中、二進(jìn)制文件完全都存在的情況下,ccache比原生的提高一倍以上, 二進(jìn)制會(huì)比ccache編譯時(shí)間再提高一倍,且穩(wěn)定在2分鐘左右。二進(jìn)制在之后的表現(xiàn)更趨于平穩(wěn),而ccache在修改了某個(gè)被引用較多的文件時(shí)、如底層的公共文件后,命中率就會(huì)大大地降低,有時(shí)會(huì)比不用ccache更耗時(shí),如#4位置。在ci有多個(gè)job同時(shí)并發(fā)在跑的情況下,由于ccache 需要對(duì)IO頻繁地讀寫操作,耗時(shí)表現(xiàn)可能會(huì)更糟糕些,我們經(jīng)常遇到過等了七十幾分鐘才出包的情況。
二進(jìn)制的編譯時(shí)間相對(duì)平穩(wěn)很多(藍(lán)色曲線),在我們架構(gòu)強(qiáng)有力的支撐下,劃分出110多個(gè)獨(dú)立組件,每次的打包基本上是就耗在某個(gè)組件的編譯+archive。如果是某些變更比較頻繁的組件,我們還可以考慮對(duì)顆粒較大組件配上ccache,做雙層編譯緩存
。雙層編譯緩存原理是Pods組件庫(kù)無(wú)二進(jìn)制組件采用源碼編譯時(shí),源碼編譯同時(shí)應(yīng)用ccache緩存支持,加速源碼組件的編譯。
同時(shí)組件庫(kù)可以配合Gitlab-Ci的runner的應(yīng)用,每次已提交代碼就觸發(fā)獨(dú)立組件的制作二進(jìn)制,讓每次的編譯速度都達(dá)到最快,藍(lán)色二進(jìn)制曲線將會(huì)更接近直線。Gitlab-Ci具體的使用教程參見后文。
如果存在有獨(dú)立組件無(wú)法編譯問題和版本依賴問題,也可以再跑個(gè)定時(shí)Job,或者其他輪詢條件Job,及時(shí)提供最新二進(jìn)制組件。
2.2、多項(xiàng)目情況
一臺(tái)機(jī)器上多個(gè)項(xiàng)目的ccache顯得是比較吃力的,且不穩(wěn)定,超出ccache的緩存最大值就會(huì)被清掉。
使用了二進(jìn)制后,即使是多個(gè)項(xiàng)目編譯時(shí)間都是趨于比較平穩(wěn)的。這里面的原理估計(jì)大家都能想得到為什么。
3、開發(fā)使用效果 - 10倍以上的提升
在Podfile引入插件后,在pod install/update后,符合條件的情況下,會(huì)自動(dòng)轉(zhuǎn)換為二進(jìn)制組件。
在我們的開發(fā)機(jī)器(iMac :Retina 5K, 27-inch, 2017 融合硬盤;)上,全量代碼之前Build需要30min+,現(xiàn)在使用全部使用二進(jìn)制后,編譯最快只需要2min+
就可以,提高的效率達(dá)到10倍
以上。
當(dāng)您在使用獨(dú)立組件庫(kù)編譯開發(fā)的時(shí)候,其實(shí)不妨試試這個(gè)二進(jìn)制的方案去跑整個(gè)項(xiàng)目,說(shuō)不定二進(jìn)制的方案比獨(dú)立組件庫(kù)跑起來(lái)還迅速。
3.1.源碼編譯
Ps:110+個(gè)Pods庫(kù)中,有20+
個(gè)穩(wěn)定Pods庫(kù)已經(jīng)被制作為二進(jìn)制庫(kù),并非全部源碼編譯,如何全部轉(zhuǎn)換為源碼編譯,實(shí)際數(shù)字會(huì)比這多出很多。
3.2. 二進(jìn)制編譯 - 全量最快2分鐘
Ps:有2個(gè)Pods
和5個(gè)Action Extension
使用源碼
編譯,其他全部是二進(jìn)制Pods。
在二進(jìn)制Build127秒中(arm64和armv7)
,除了源碼編譯的時(shí)間外,約45秒
消耗在copy pods Resource
。
實(shí)際在編譯模擬器x86_64架構(gòu)
時(shí)只需要90秒
不到的時(shí)間。
全量編譯中,13496個(gè)Tasks/727個(gè)Tasks
,1710秒(28.5分鐘)/127秒(2分鐘)
,編譯速度提升的速度遠(yuǎn)遠(yuǎn)超過10倍
。
3.3 演示
在環(huán)境搭建完后,開發(fā)人員在Podfile中,加入以下兩句,就能享用到自動(dòng)切換為二進(jìn)制組件,體驗(yàn)極速編譯。
plugin 'cocoapods-imy-bin'
use_binaries!
更具體情況視頻演示
4、功能點(diǎn)
目前cocoapods-imy-bin
插件支持的功能如下
- 無(wú)侵入、無(wú)影響現(xiàn)有的業(yè)務(wù)。
- 不影響未接入二進(jìn)制化方案的業(yè)務(wù)團(tuán)隊(duì),提供配置文件。
- 只要項(xiàng)目能編譯通過就制作,即使獨(dú)立組件編譯失敗。
- 支持無(wú)二進(jìn)制版本時(shí),自動(dòng)采用源碼版本。
- 支持只需項(xiàng)目能編譯通過就能制作二進(jìn)制組件,無(wú)需再關(guān)心pod lint等。
- 支持
pod bin local
命令一鍵自動(dòng)化制作、上傳、存儲(chǔ)項(xiàng)目本地已經(jīng)存在的二進(jìn)制組件,可配合ci打包的編譯產(chǎn)物使用。 - 支持指定依賴分支、支持:podspec =>'', :git 方式的引用
- 支持同時(shí) .a、Framework 靜態(tài)庫(kù)產(chǎn)出
- 支持archive時(shí),根據(jù)Podfile自動(dòng)獲取podsepc依賴的庫(kù),無(wú)需強(qiáng)制去spec倉(cāng)庫(kù)拉取。
- 支持多套隔離環(huán)境,如Debug/Release/Dev配置,方便為Debug/Release/Dev各種環(huán)境提供專用二進(jìn)制組件。
- 支持輸出.a二進(jìn)制組件制作binary.podsepc無(wú)需模板。
- 支持穩(wěn)定的二進(jìn)制組件,在上傳二進(jìn)制組件的binary.podsepc跳過pod lint驗(yàn)證,加快速度。
- 支持
pod bin auto
命令一鍵自動(dòng)化制作、上傳、存儲(chǔ)單個(gè)二進(jìn)制組件 - 支持
pod bin auto --all-make
命令一鍵自動(dòng)化制作、上傳、存儲(chǔ)該項(xiàng)目下所有組件
的二進(jìn)制組件 - 支持 是否使用二進(jìn)制文件、是否制作二進(jìn)制文件和二進(jìn)制/源碼調(diào)試功能的白名單設(shè)置
- 支持
pod install/update
多線程模式,加快pod過程,Pod速度提升80%+。 - 支持
pod bin install/update
命令,實(shí)現(xiàn)無(wú)入侵修改Podfile內(nèi)容,避免直接修改工程的Podfile文件而導(dǎo)致提交沖突、誤提交。 - 支持
pod bin code
命令,實(shí)現(xiàn)二進(jìn)制庫(kù)不切換源碼庫(kù)、程序無(wú)需重新運(yùn)行的調(diào)試能力
作者簡(jiǎn)介
蘇良錦,美柚 iOS 工程師,2019 年加入美柚。