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

Github開源地址,一步一步教你怎么使用

一、概述

經(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ǔ)言有:CC++Objective-CObjective-C++

下面這張圖基本就闡述了CCache的工作原理。

ccache工作原理

在項(xiàng)目中的實(shí)際編譯流程
Ccache實(shí)際編譯流程

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):

  1. 滿足我們追求的無(wú)侵入無(wú)影響現(xiàn)有的業(yè)務(wù)的要求,無(wú)入侵、且開發(fā)人員無(wú)感知。
  2. 確實(shí)能大幅度地提升編譯速度,美柚項(xiàng)目上最快時(shí)提高3倍以上的編譯速度。
  3. 不需要對(duì)項(xiàng)目作出大調(diào)整,只需部署相關(guān)環(huán)境和一些腳本支持。
  4. 不需要改變開發(fā)工具鏈。
  5. 同一個(gè)目錄下,CCache 的緩存命中率相對(duì)穩(wěn)定。

對(duì)我們項(xiàng)目中有存在些問題點(diǎn):

  1. 在未有緩存的情況下,首次打包編譯的時(shí)間比原來(lái)的翻近一倍,原來(lái)20+min,首次將近40+min,在資源緊張的情況下,甚至是70min+。
  2. 修改一些引用較多的文件(如公共庫(kù)、底層庫(kù)改動(dòng)),容易造成大范圍的緩存失效,速度會(huì)變得比原來(lái)未使用ccache時(shí)更慢。
  3. 多個(gè)項(xiàng)目相同的組件不支持緩存共享,我們有多個(gè)分支打包的需求,修改目錄名稱后,緩存即失效。
  4. 我們機(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清緩存。
  5. 對(duì)機(jī)器硬盤讀寫要求高,如不是全部固態(tài)硬盤,速度影響大。
  6. CCache 不支持 Clang Modules,系統(tǒng)框架例如 AVFoundation、CoreLocation等, Xcode 不會(huì)再幫你自動(dòng)引入,會(huì)導(dǎo)致編譯失敗。
  7. CCache 不支持 PCH 文件
  8. CCache 目前不支持 Swift
CCache與原生編譯時(shí)間曲線圖

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分鐘一次

arm64和armv7架構(gòu)

上圖是個(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)目情況

Ccache多項(xiàng)目編譯時(shí)間曲線圖
二進(jìn)制多項(xiàng)目編譯時(shí)間曲線圖

一臺(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ì)比這多出很多。

源碼編譯Tasks
源碼編譯時(shí)間

3.2. 二進(jìn)制編譯 - 全量最快2分鐘

Ps:有2個(gè)Pods5個(gè)Action Extension使用源碼編譯,其他全部是二進(jìn)制Pods。

二進(jìn)制編譯Tasks
二進(jìn)制編譯時(shí)間

在二進(jìn)制Build127秒中(arm64和armv7),除了源碼編譯的時(shí)間外,約45秒消耗在copy pods Resource

實(shí)際在編譯模擬器x86_64架構(gòu)時(shí)只需要90秒不到的時(shí)間。

全量編譯中,13496個(gè)Tasks/727個(gè)Tasks1710秒(28.5分鐘)/127秒(2分鐘),編譯速度提升的速度遠(yuǎn)遠(yuǎn)超過10倍

3.3 演示

Pods自動(dòng)切換為二進(jìn)制組件
二進(jìn)制使用演示

在環(huán)境搭建完后,開發(fā)人員在Podfile中,加入以下兩句,就能享用到自動(dòng)切換為二進(jìn)制組件,體驗(yàn)極速編譯。

plugin 'cocoapods-imy-bin' 
use_binaries!

更具體情況視頻演示

4、功能點(diǎn)

目前cocoapods-imy-bin插件支持的功能如下

  1. 無(wú)侵入、無(wú)影響現(xiàn)有的業(yè)務(wù)。
  2. 不影響未接入二進(jìn)制化方案的業(yè)務(wù)團(tuán)隊(duì),提供配置文件。
  3. 只要項(xiàng)目能編譯通過就制作,即使獨(dú)立組件編譯失敗。
  4. 支持無(wú)二進(jìn)制版本時(shí),自動(dòng)采用源碼版本。
  5. 支持只需項(xiàng)目能編譯通過就能制作二進(jìn)制組件,無(wú)需再關(guān)心pod lint等。
  6. 支持pod bin local 命令一鍵自動(dòng)化制作、上傳、存儲(chǔ)項(xiàng)目本地已經(jīng)存在的二進(jìn)制組件,可配合ci打包的編譯產(chǎn)物使用。
  7. 支持指定依賴分支、支持:podspec =>'', :git 方式的引用
  8. 支持同時(shí) .a、Framework 靜態(tài)庫(kù)產(chǎn)出
  9. 支持archive時(shí),根據(jù)Podfile自動(dòng)獲取podsepc依賴的庫(kù),無(wú)需強(qiáng)制去spec倉(cāng)庫(kù)拉取。
  10. 支持多套隔離環(huán)境,如Debug/Release/Dev配置,方便為Debug/Release/Dev各種環(huán)境提供專用二進(jìn)制組件。
  11. 支持輸出.a二進(jìn)制組件制作binary.podsepc無(wú)需模板。
  12. 支持穩(wěn)定的二進(jìn)制組件,在上傳二進(jìn)制組件的binary.podsepc跳過pod lint驗(yàn)證,加快速度。
  13. 支持pod bin auto 命令一鍵自動(dòng)化制作、上傳、存儲(chǔ)單個(gè)二進(jìn)制組件
  14. 支持pod bin auto --all-make 命令一鍵自動(dòng)化制作、上傳、存儲(chǔ)該項(xiàng)目下所有組件的二進(jìn)制組件
  15. 支持 是否使用二進(jìn)制文件、是否制作二進(jìn)制文件和二進(jìn)制/源碼調(diào)試功能的白名單設(shè)置
  16. 支持pod install/update多線程模式,加快pod過程,Pod速度提升80%+
  17. 支持pod bin install/update 命令,實(shí)現(xiàn)無(wú)入侵修改Podfile內(nèi)容,避免直接修改工程的Podfile文件而導(dǎo)致提交沖突、誤提交。
  18. 支持pod bin code命令,實(shí)現(xiàn)二進(jìn)制庫(kù)不切換源碼庫(kù)、程序無(wú)需重新運(yùn)行的調(diào)試能力

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

Github開源地址


作者簡(jiǎn)介
蘇良錦,美柚 iOS 工程師,2019 年加入美柚。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。