iOS編譯速度如何穩(wěn)定提高10倍以上之一

作者 | Mr_Coder
來源 | 掘金
https://juejin.cn/post/6903407900006449160
一、概述
經(jīng)過多年的發(fā)展,美柚iOS項目代碼已經(jīng)達到40W行+的規(guī)模,所使用的 Pod 庫的數(shù)量達到了110+,App Store 安裝包210M+,在這么大的項目規(guī)模下(CI機器 MAC配置:3 GHz 8-Core Intel Xeon E5;時間:發(fā)布20min+),(開發(fā)機器iMac :Retina 5K, 27-inch, 2017 融合硬盤;時間:build30min+)打包、編譯問題逐步成為我們團隊一個躲不過的痛,嚴重影響了我們的研發(fā)效率與其他團隊之間的協(xié)作。
我們一臺13年的ci機器同時需要承接七八個項目、多個分支的打包任務(wù),在有多個項目同時打包的情況,顯得尤其地力不從心。
在硬件資源有限的情況下,并且在無侵入、無影響現(xiàn)有的業(yè)務(wù)的前提下,如何解決這些擺在團隊面前的難題,便成了我們迫在眉睫的迫切需求,最近半年多來一直在尋找加快打包速度的方案。
二、編譯提速探索與嘗試
1、CCache
CCache 是一個編譯緩存器,一個能夠把編譯的中間產(chǎn)物緩存起來的工具
其原理是通過把項目的源文件用ccache編譯器編譯,然后緩存編譯生成的信息,從而在下一次編譯時,利用這個緩存加快編譯的速度,目前支持的語言有:C、C++、Objective-C、Objective-C++
下面這張圖基本就闡述了CCache的工作原理。

在項目中的實際編譯流程

Ccache我們經(jīng)過在工程的一番嘗試、確實在某些方面上極大的提升了我們出包的速度。美柚iOS Ci打包從之前的最快20min+出包到最快10min,確實能夠給我們帶來比較不錯的提升,大大加快了我們項目的出包速度。在我們項目運行了幾個月后,對于我們項目的情況,也發(fā)現(xiàn)了一些問題,現(xiàn)在總結(jié)了以下幾點:
優(yōu)點:
滿足我們追求的無侵入、無影響現(xiàn)有的業(yè)務(wù)的要求,無入侵、且開發(fā)人員無感知。
確實能大幅度地提升編譯速度,美柚項目上最快時提高3倍以上的編譯速度。
不需要對項目作出大調(diào)整,只需部署相關(guān)環(huán)境和一些腳本支持。
不需要改變開發(fā)工具鏈。
同一個目錄下,CCache 的緩存命中率相對穩(wěn)定。
對我們項目中有存在些問題點:
在未有緩存的情況下,首次打包編譯的時間比原來的翻近一倍,原來20+min,首次將近40+min,在資源緊張的情況下,甚至是70min+。
修改一些引用較多的文件(如公共庫、底層庫改動),容易造成大范圍的緩存失效,速度會變得比原來未使用ccache時更慢。
多個項目相同的組件不支持緩存共享,我們有多個分支打包的需求,修改目錄名稱后,緩存即失效。
我們機器的Ccache最大的緩存上限約18GB,且Debug/Release區(qū)別緩存,美柚iOS項目占用5GB+的緩存,多個項目、多個分支很容易超出上限,一臺Ci機器同時支持多個項目會觸發(fā)CCache清緩存。
對機器硬盤讀寫要求高,如不是全部固態(tài)硬盤,速度影響大。
CCache 不支持 Clang Modules,系統(tǒng)框架例如 AVFoundation、CoreLocation等, Xcode 不會再幫你自動引入,會導(dǎo)致編譯失敗。
CCache 不支持 PCH 文件
CCache 目前不支持 Swift

2、靜態(tài)庫二進制方案的探索
雖然我們已經(jīng)在Ci的在應(yīng)用了Ccache已經(jīng)有提升近一倍的出包速度了,但是存在的問題也比較明顯。
在去年的某次技術(shù)周會上,我們的大佬提出了使用二進制編譯的自研任務(wù),可以更進一步提高研發(fā)效率。得到了大佬的啟發(fā)后,就一直在實踐與探索二進制之路上。
我們的項目使用 CocoaPods 來管理第三方庫和私有庫的依賴,對大部分項目來說應(yīng)該是標配了。目前還是純 Objective-C 的項目,有少量C++,暫沒有引入 Swift。
3、 調(diào)研過的二進制組件方案
下面列出研究過的一些主流方案以及最后沒有采用的原因,這些方案有各自的局限性,但是也給了我不少啟發(fā),思考過程跟最終方案一樣有價值。
3.1、Carthage
Carthage可以將一部分不常變的庫打包成framework,再引如到主工程,這樣可以減少開發(fā)過程中的編譯時間。Carthage 可以比較方便地調(diào)試源碼。因為我們目前已經(jīng)大規(guī)模使用 CocoaPods,轉(zhuǎn)用 Carthage 來做包管理需要做大量的轉(zhuǎn)換工作,變動太大,不滿足我們的無侵入、無影響現(xiàn)有的業(yè)務(wù),所以不考慮這個方案了。
3.2、cocoapods-packager
cocoapods-packager 可以將任意的 pod 打包成 Static Library,省去重復(fù)編譯的時間,一定程度上可以加快編譯時間,但是也有自身的問題:
優(yōu)化不徹底,只能優(yōu)化第三方和私有 Pod 的編譯速度,對于其他改動頻繁的業(yè)務(wù)代碼無能為力
私有庫和第三方庫的后續(xù)更新很麻煩,當有源碼修改后,需要重新打包上傳到內(nèi)部的 Git 倉庫
過多的二進制文件會拖慢 Git 的操作速度(目前還沒部署 Git 的 LFS)
難以調(diào)試源碼,不共享編譯緩存
打包成 Static Library 過程緩慢,需要通過pod lint,各個組件間又層層嵌套依賴,在現(xiàn)有階段來說,是難以實現(xiàn)的。
3.3、cocoapods-binary
Cocoapods-Binary(Cocoapods 官方推薦的二進制插件), 是一個即時生成二進制包并緩存,而非像 CocoaPods-Packager 僅僅針對單個私有庫的。原理是通過 CocoaPods 提供的 pre_install hook 在 pod install 的 prepare 階段攔截到當前的 pod install context,進而 fork 出一份獨立的 installer 以完成將預(yù)編譯源碼 clone 至 Pod/_Prebuild 目錄下,同時也存在幾個不足之處:
單私有源,無法實現(xiàn)服務(wù)端緩存,在沒有對應(yīng)二進制包版本時,pod install 后會額外去做二進制包的生成,一定程度上會影響 pod install的速度。
開發(fā)者切回源碼調(diào)試,二進制緩存會一并清空,需求重新編譯。
多個項目、不同分支的相同組件依舊無法共享
只支持framework,對我們項目現(xiàn)狀需要比較大的頭文件引用方式改動。
3.4、cocoapods-bin 雙私有源
該插件進行二進制化的策略是采用雙私有源,即2個源地址,一個靜態(tài)服務(wù)器保存預(yù)先打好包的framework,一個是我們現(xiàn)在保存源碼的服務(wù)地址,在install的時候去選擇使用下載那個,是個很不錯的項目,深受啟發(fā)。
優(yōu)點:
源碼和二進制文件之間可以來回切換,速度比較快
不影響未接入二進制化方案的業(yè)務(wù)團隊
無二進制版本時,自動采用源碼版本
接近原生 CocoaPods 的使用體驗
對于在我們項目中存在的不足之處:
不支持指定分支,:podspec =>'', :git 方式的引用,對需要支持多個分支、多個業(yè)務(wù)線的項目是致命的。
Archive二進制文件時,只能去spec倉庫下載源碼,無法根據(jù)指定的分支去下載依賴庫,導(dǎo)致編譯失敗、錯亂的問題
依賴的組件需要推送到spec倉庫,很多私有庫并沒有推送到倉庫,且對于頻繁改動的私有庫,推送到倉庫的verify很慢且與我們的開發(fā)習慣不符。
不支持.a靜態(tài)文件輸出,項目中大量類似 #import "IMYPulic.h"需要一個個庫去編譯替換為#import
,想想那110多個組件庫~ 只支持一套環(huán)境,對于有Debug/Release/Dev開發(fā)環(huán)境需求的無法滿足
不支持二進制組件的源碼調(diào)試
不能流暢的支持頻繁變動的業(yè)務(wù)組件,操作會異常繁瑣。
針對于我們的項目,目前存在較大的障礙,無法使用起來。
4、 思考與總結(jié)
經(jīng)過一個多月來對業(yè)界存在的輪子的分析和思考,并在一定的實踐后,最后我們決定自己造一個靈活的、可配置的、簡便的、無入侵的、雙私有源二進制組件輔助插件。
接下來就擼起袖子,努力干吧~,騷年
三、雙私有源二進制組件簡介
在受到cocoapod-bin啟發(fā)后,在借鑒它的部分框架下,我們實現(xiàn)了自己的二進制輔助插件cocoapods-imy-bin,并新增了幾個命令和二進制源碼調(diào)試能力。
1、能做什么?只要能編譯通過,就制作
在cocoapods-imy-bin的輔助下,能無侵入式自動化地制作所有符合條件的組件為二進制,且對于頻繁的業(yè)務(wù)組件也能輕松的應(yīng)用上二進制組件,無需多余操作,一切交給cocoapods-imy-bin自動化運行。
同時對于研發(fā)人員,也能提供獨立的二進制組件給研發(fā)人員使用,解決日常的編譯 效率、跑真機效率低下,被墻等各種問題。
我們的口號是:只要能編譯通過,就制作。一次編譯到處使用,無入侵。
即使獨立的組件庫編譯不通過,整體項目能編譯通過也制作。
整套環(huán)境下來,沒有讓我們的開發(fā)人員改變原來的開發(fā)習慣,沒有改動業(yè)務(wù)中相關(guān)的代碼,基本上做到了使用人員無感知狀態(tài)。
2、Ci打包效果
2.1 單項目 - 編譯最快2分鐘一次

上圖是個由我們打了幾千個包的經(jīng)驗得出對單個項目編譯時間大致的曲線圖。這里假設(shè)一臺機器只一次只有一次job。Y軸編譯時間,X軸某次的編譯, 紅色線條表示的是原生(未使用Ccache和二進制組件),黃色線表示使用了Ccache,藍色表示使用了二進制組件。
由圖可以看出來在無任何輔助下原生的編譯時間曲線(紅色)是趨于平緩,在20min上下左右。Ccache和二進制第一次在無任何緩存的情況下,在一定程度上是會比原生的耗時,Ccache主要耗時在邊編譯邊緩存項目的編譯產(chǎn)物。二進制主要耗時在編譯完成后,對.a編譯產(chǎn)物的組裝和push到私有源倉庫的時間上(這個跟所采用有關(guān)系,如果沒有利用Jenkins 編譯后的產(chǎn)物制作二進制就不存在。)。
在ccache完全命中、二進制文件完全都存在的情況下,ccache比原生的提高一倍以上, 二進制會比ccache編譯時間再提高一倍,且穩(wěn)定在2分鐘左右。二進制在之后的表現(xiàn)更趨于平穩(wěn),而ccache在修改了某個被引用較多的文件時、如底層的公共文件后,命中率就會大大地降低,有時會比不用ccache更耗時,如#4位置。在ci有多個job同時并發(fā)在跑的情況下,由于ccache 需要對IO頻繁地讀寫操作,耗時表現(xiàn)可能會更糟糕些,我們經(jīng)常遇到過等了七十幾分鐘才出包的情況。
二進制的編譯時間相對平穩(wěn)很多(藍色曲線),在我們架構(gòu)強有力的支撐下,劃分出110多個獨立組件,每次的打包基本上是就耗在某個組件的編譯+archive。如果是某些變更比較頻繁的組件,我們還可以考慮對顆粒較大組件配上ccache,做雙層編譯緩存。雙層編譯緩存原理是Pods組件庫無二進制組件采用源碼編譯時,源碼編譯同時應(yīng)用ccache緩存支持,加速源碼組件的編譯。
同時組件庫可以配合Gitlab-Ci的runner的應(yīng)用,每次已提交代碼就觸發(fā)獨立組件的制作二進制,讓每次的編譯速度都達到最快,藍色二進制曲線將會更接近直線。Gitlab-Ci具體的使用教程參見后文。
如果存在有獨立組件無法編譯問題和版本依賴問題,也可以再跑個定時Job,或者其他輪詢條件Job,及時提供最新二進制組件。
2.2、多項目情況


一臺機器上多個項目的ccache顯得是比較吃力的,且不穩(wěn)定,超出ccache的緩存最大值就會被清掉。
使用了二進制后,即使是多個項目編譯時間都是趨于比較平穩(wěn)的。這里面的原理估計大家都能想得到為什么。
3、開發(fā)使用效果 - 10倍以上的提升
在Podfile引入插件后,在pod install/update后,符合條件的情況下,會自動轉(zhuǎn)換為二進制組件。
在我們的開發(fā)機器(iMac :Retina 5K, 27-inch, 2017 融合硬盤;)上,全量代碼之前Build需要30min+,現(xiàn)在使用全部使用二進制后,編譯最快只需要2min+就可以,提高的效率達到10倍以上。
當您在使用獨立組件庫編譯開發(fā)的時候,其實不妨試試這個二進制的方案去跑整個項目,說不定二進制的方案比獨立組件庫跑起來還迅速。
3.1.源碼編譯
Ps:110+個Pods庫中,有20+個穩(wěn)定Pods庫已經(jīng)被制作為二進制庫,并非全部源碼編譯,如何全部轉(zhuǎn)換為源碼編譯,實際數(shù)字會比這多出很多。

3.2. 二進制編譯 - 全量最快2分鐘
Ps:有2個Pods和5個Action Extension使用源碼編譯,其他全部是二進制Pods。


在二進制Build 127秒中(arm64和armv7),除了源碼編譯的時間外,約45秒消耗在copy pods Resource。
實際在編譯模擬器x86_64架構(gòu)時只需要90秒不到的時間。
全量編譯中,13496個Tasks/727個Tasks,1710秒(28.5分鐘)/127秒(2分鐘),編譯速度提升的速度遠遠超過10倍。
3.3 演示

在環(huán)境搭建完后,開發(fā)人員在Podfile中,加入以下兩句,就能享用到自動切換為二進制組件,體驗極速編譯。
plugin 'cocoapods-imy-bin'
use_binaries!
更具體情況視頻演示
4、功能點
目前cocoapods-imy-bin插件支持的功能如下
無侵入、無影響現(xiàn)有的業(yè)務(wù)。
不影響未接入二進制化方案的業(yè)務(wù)團隊,提供配置文件。
只要項目能編譯通過就制作,即使獨立組件編譯失敗。
支持無二進制版本時,自動采用源碼版本。
支持只需項目能編譯通過就能制作二進制組件,無需再關(guān)心pod lint等。
支持pod bin local 命令一鍵自動化制作、上傳、存儲項目本地已經(jīng)存在的二進制組件,可配合ci打包的編譯產(chǎn)物使用。
支持指定依賴分支、支持:podspec =>'', :git 方式的引用
支持同時 .a、Framework 靜態(tài)庫產(chǎn)出
支持archive時,根據(jù)Podfile自動獲取podsepc依賴的庫,無需強制去spec倉庫拉取。
支持多套隔離環(huán)境,如Debug/Release/Dev配置,方便為Debug/Release/Dev各種環(huán)境提供專用二進制組件。
支持輸出.a二進制組件制作binary.podsepc無需模板。
支持穩(wěn)定的二進制組件,在上傳二進制組件的binary.podsepc跳過pod lint驗證,加快速度。
支持pod bin auto 命令一鍵自動化制作、上傳、存儲單個二進制組件
支持pod bin auto --all-make 命令一鍵自動化制作、上傳、存儲該項目下所有組件的二進制組件
支持 是否使用二進制文件、是否制作二進制文件和二進制/源碼調(diào)試功能的白名單設(shè)置
支持pod install/update 多線程模式,加快pod過程,Pod速度提升80%+。
支持pod bin install/update 命令,實現(xiàn)無入侵修改Podfile內(nèi)容,避免直接修改工程的Podfile文件而導(dǎo)致提交沖突、誤提交。
支持pod bin code命令,實現(xiàn)二進制庫不切換源碼庫、程序無需重新運行的調(diào)試能力
-End-
最近有一些小伙伴,讓我?guī)兔φ乙恍?nbsp;面試題 資料,于是我翻遍了收藏的 5T 資料后,匯總整理出來,可以說是程序員面試必備!所有資料都整理到網(wǎng)盤了,歡迎下載!

面試題】即可獲取