如何提升iOS 工程打包速度

過慢的編譯速度有非常明顯的副作用。一方面,程序員在等待打包的過程中可能會分心,比如刷刷朋友圈,看條新聞等等。這種認知上下文的切換會帶來很多隱形的時間浪費。另一方面,大部分 app 都有自己的持續(xù)集成工具,如果打包速度太慢, 會影響整個團隊的開發(fā)進度。

因此,本文會分別討論日常開發(fā)和持續(xù)集成這兩種場景,分析打包速度慢的瓶頸所在,以及對應(yīng)的解決方案。利用這些方案,筆者成功的把公司 app 的持續(xù)集成時間從 45 min 成功的減少到 9 min,效率提升高達 80%,理論上打包速度可以提升 10 倍以上。如果用一句話總結(jié)就是:

在絕對的實力(硬件)面前,一切技巧(軟件)都是浮云

日常開發(fā)

其實日常開發(fā)的優(yōu)化空間并不大,因為默認情況下 Xcode 會使用上次編譯時留下的緩存,也就是所謂的增量編譯。因此,日常開發(fā)的主要耗時由三部分構(gòu)成:

總耗時 = 增量編譯 + 鏈接 + 生成調(diào)試信息(dSYM)

這里的增量編譯耗時比較短,即使是在我 14 年高配的 MacBook Pro(4核心,8 線程,2.5GHz i7 4870HQ,下文簡稱 MBP) 上,也僅僅耗時十秒上下。我們的應(yīng)用代碼量大約一百多萬行,業(yè)內(nèi)超過這個量級的應(yīng)用應(yīng)該不多。鏈接和生成調(diào)試信息各花費不到 20s,因此一次增量的編譯的時間開銷在半分鐘到一分鐘左右,我們逐個分析:

增量編譯: 因為耗時較短(大概十幾秒或者更少),幾乎不存在優(yōu)化的空間,但是非常容易惡化。因為只有頭文件不變的編譯單元才能被緩存,如果某個文件被 N 個文件引用,且這個文件的頭文件發(fā)生了變化,那么這 N 個文件都會重編譯。APP 的分層架構(gòu)一般都會做,但一個典型的誤區(qū)是在基礎(chǔ)庫的頭文件中使用宏定義,比如定義一些全局都可以讀取的常量,比如是否開啟調(diào)試,服務(wù)器的地址等等。這些常量一旦改變(比如為了調(diào)試或者切換到某些分支)就會導致應(yīng)用重編譯。

鏈接:鏈接沒有緩存,而且只能用單核進行,因此它的耗時主要取決于單核性能和磁盤讀寫速度。考慮到我們的目標文件一般都比較小,因此 4K 隨機讀寫的性能應(yīng)該會更重要一些。

調(diào)試信息:日常開發(fā)時,并不需要生成 dSYM 文件,這個文件主要用于崩潰時查找調(diào)用棧,方便線上應(yīng)用進行調(diào)試,而開發(fā)過程中的崩潰可以直接在? Xcode 中看到,關(guān)閉這個功能不會對開發(fā)產(chǎn)生任何負面影響。

日常開發(fā)的優(yōu)化空間不大,即使是龐大的項目,落后的機器性能,關(guān)閉 dSYM 以后也就耗時 30s 左右。相比之下,打包速度可以優(yōu)化和討論的地方就比較多了。

持續(xù)集成

在利用 Jenkins 等工具進行持續(xù)集成時,緩存不推薦被使用。這是因為蘋果的緩存不夠穩(wěn)定,在某些情況下還存在 bug。比如明明本地已經(jīng)修復了 bug,可以編譯通過,但上次的編譯緩存沒有被正確清理,導致在打包機器上依然無法編譯通過?;蛘弑镜孛髅鲗懗隽?bug,但同樣由于緩存問題,打包機器依然可以編譯通過。

因此,無論是手動刪除Derived Data文件夾,還是調(diào)用xcodebuild clean命令,都會把緩存清空。或者直接使用xcodebuild archive,會自動忽略緩存。每次都要全部重編譯是導致打包速度慢的根本原因。以我們的項目為例,總計 45min 的打包時間中,有 40min 都在執(zhí)行xcodebuild這一行命令。

使用 CCache 緩存

最自然的想法就是使用緩存了,既然蘋果的緩存不靠譜,那么就找一個靠譜的緩存,比如 CCache。它是基于編譯器層面的緩存,根據(jù)目前反饋的情況看,并不存在緩存不一致的問題。根據(jù)筆者的實驗,使用 CCache 確實能夠較大幅度的提升打包速度,刪除緩存并使用 CCache 重編譯后,耗時只有十幾分鐘。

然而,CCache 最致命的問題是不支持 PCH 文件和 Clang modules。PCH 的本意是優(yōu)化編譯時間,我們假設(shè)有一個頭文件 A 依賴了 M 個頭文件,其中每個被依賴的頭文件又依賴了 N 個 頭文件,如下圖所示:

由于#import的本質(zhì)就是把被依賴頭文件的內(nèi)容拷貝到自己的頭文件中來,因此頭文件 A 中實際上包含了 M * N 個頭文件的內(nèi)容,也就需要 M * N? 次文件 IO 和相關(guān)處理。當項目中每增加一個依賴頭文件 A 的文件,就會重復一次上述的 M * N? 復雜度的過程。

PCH 文件的好處是,這個文件中的頭文件只會被編譯一次并緩存下來,然后添加到項目中所有的頭文件中去。上述問題倒是解決了,但很智障的一點是,所有文件都會隱式的依賴所有 PCH 中的文件,而真正需要被全局依賴的文件其實非常少。因此實際開發(fā)中,更多的人會把 PCH 當成一種快速import的手段,而非編譯性能的優(yōu)化。前文解釋過,PCH 文件一旦發(fā)生修改,會導致徹徹底底,完完整整的項目重編譯,從而降低編譯速度。正是因為 PCH 的副作用甚至抵消了它帶來的優(yōu)化,蘋果已經(jīng)默認不使用 PCH 文件了。

用來取代 PCH 的就是 Clang modules 技術(shù),對于開啟了這一選項的項目,我們可以用@import來替代過去的#import,比如:

@import UIKit;

等價于

#import

拋開自動鏈接 framework 這些小特性不談,Clang modules 可以理解為模塊化的 PCH,它具備了 PCH 可以緩存頭文件的優(yōu)點,同時提供了更細粒度的引用。

說回到 CCache,由于它不支持 PCH 和 Clang modules,導致無法在我們的項目中應(yīng)用。即使可以用,也會拖累項目的技術(shù)升級,以這種代價來換取緩存,只怕是得不償失。

distcc

distcc 是一種分布式編譯工具,可以把需要被編譯的文件發(fā)送到其他機器上編譯,然后接收編譯產(chǎn)物。然而,經(jīng)過貼吧、貝聊、手Q 等應(yīng)用的多方實驗,發(fā)現(xiàn)并不適合 iOS 應(yīng)用。它的原理是多個客戶端共同編譯,但是絕大多數(shù)文件其實編譯時間非常短,并不值得通過網(wǎng)絡(luò)來回傳送,這種方案應(yīng)該只適合單個文件體量非常大的項目。在我們的項目中,使用distcc大幅度增加了打包時間,大約耗時 1 小時左右。

定位瓶頸

在尋求外部工具無果后,筆者開始嘗試著對編譯時間直接做優(yōu)化。為了搞清楚這 40min 究竟是如何花費的,我首先對xcodebuild的輸出結(jié)果進行詳細分析。

使用過xcodebuild命令的人都會知道,它的輸出結(jié)果對開發(fā)者并不友好,幾乎沒有可讀性,好在還有xcpretty這個工具可以格式化它:

gem install xcpretty

通過gem安裝后,只要把xcodebuild的輸出結(jié)果通過管道傳給xcpretty即可:

xcodebuild -scheme Release ... | xcpretty

下面是官方文檔中的 Demo:

我只對其中的編譯部分感興趣,所以簡單的做下過濾,我們就可以得到格式高度統(tǒng)一的輸出:

Compiling A.m

Compiling B.m

Compiling ...

Compiling N.m

到了這一步,終于可以做最關(guān)鍵的計算了,我們可以通過設(shè)置定時器,計算相鄰兩行輸出之間的間隔,這個間隔就是文件的編譯時間。當然,也有類似的輔助工具做好了這個邏輯:

npm install gnomon

簡單的做一下排序,就可以看到最耗時的前 200 個文件了,還可以針對文件后綴作區(qū)分,計算總耗時等等。經(jīng)過排查,我們發(fā)現(xiàn)一半的編譯時間都花在了編譯 protobuf 文件上。

工程設(shè)置

除了針對超長耗時的文件進行 case-by-case 的分析外,另一種方案是調(diào)整工程設(shè)置。一般來說,我們的持續(xù)集成工具主要是用來給產(chǎn)品經(jīng)理或者測試人員使用,用來體驗功能或者驗證 Bug,除非是需要上架 App Store,否則并不需要關(guān)心運行時性能。然而在手機上使用的 Release 模式,默認會開啟各種優(yōu)化,這些優(yōu)化都是犧牲編譯性能,換取運行時速度,對于上架的包而言無可厚非,但對于那些 Daily Build 包來說,就顯得得不償失了。

因此,加速打包的思路和優(yōu)化的思路是完全互逆的,我們要做的就是關(guān)閉一切可能的優(yōu)化。這里推薦一篇文章:關(guān)于Xcode編譯性能優(yōu)化的研究工作總結(jié),可以說相當全面了。

經(jīng)過對其中各個參數(shù)的查找資料和嘗試關(guān)閉,按照提升速度的降序排列,簡單整理幾個:

僅支持 armv7 指令集。手機上的指令集都屬于 ARM 系列,從老到新依次是 armv7、armv7s 和 arm64。新的指令集可以兼容舊的機型,但舊的機型不能兼容新的指令集。默認情況下我們打出來的包會有 armv7 和 arm64 兩種指令集, 前者負責兜底,而對于支持 arm64 指令集的機型來說,使用最新的指令集可以獲得更好的性能。當然代價就是生成兩種指令集花費了更多時間。所以在急速打包模式下,我們只生成 armv7 這種最老的指令集,犧牲了運行時性能換取編譯速度。

關(guān)閉編譯優(yōu)化。優(yōu)化的基本原理是犧牲編譯時性能,追求運行時性能。常見的優(yōu)化有編譯時刪除無用代碼,保留調(diào)試信息,函數(shù)內(nèi)聯(lián)等等。因此提升打包速度的秘訣就是反其道而行之,犧牲運行時性能來換取編譯時性能。筆者做的兩個最主要的優(yōu)化是把Optimize level改成 O0,表示不做任何優(yōu)化。

使用虛擬磁盤。編譯過程中需要大量的磁盤 IO,這主要發(fā)生在Derived Data目錄下,因此如果內(nèi)存足夠,可以考慮劃出 4G 左右的內(nèi)存,建一個虛擬磁盤,這樣將會把磁盤 IO 優(yōu)化為 內(nèi)存 IO,從而提高速度。由于打包機器每次都會重編譯,因此并不需要擔心重啟機器后緩存丟失的問題。

不生成 dYSM 文件,前文已經(jīng)介紹過。

一些其他的選項,參考前面推薦的文章。

在以上幾個操作中,精簡指令集的作用最大,大約可以把編譯時間從 45 min 減少到 30min 以內(nèi),配合關(guān)閉編譯優(yōu)化,可以進一步把打包時間減少到 20min。虛擬磁盤大約可以減少兩三分鐘的編譯時間,dSYM 耗時大約二十秒,其它選項的優(yōu)化程度更低,大約在幾秒左右,沒有精確測算。

因此,一般來說只要精簡指令集并關(guān)閉優(yōu)化即可,有條件的機器可以使用虛擬磁盤,不建議再做其它修改。

二進制化

二進制化主要指的是利靜態(tài)庫代替源碼,避免編譯。前文已經(jīng)介紹過如何分析文件的耗時,因此二進制化的收益非常容易計算出來。由于團隊分工問題,筆者沒有什么二進制化的經(jīng)驗,一般來說這個優(yōu)化比較適合基礎(chǔ)架構(gòu)組去實施。

硬件加速

以上主要是通過修改軟件的方式來加速打包,自從公司申請了 2013 年款 Mac Pro(Xeon-E5 1630 6 核 12 線程,16G 內(nèi)存,256G SSD 標配,下文簡稱 Mac Pro)后,不需要修改任何配置,僅僅是簡單的遷移打包機器,就可以把打包時間降低到 15 min,配和上一節(jié)中的前三條優(yōu)化,最終的打包時間大概在 10min 以內(nèi)。

在我的黑蘋果(i7 7820x 8 核 16 線程,16G 內(nèi)存,三星? PM 961 512G SSD,下文簡稱黑蘋果)上,即使不開啟任何優(yōu)化,從零開始編譯也僅需 5min。如果將 protobuf 文件二進制化,再配合一些工程設(shè)置的優(yōu)化,我不敢想象需要花多長時間,預(yù)計在 4min 左右吧,速度提升了大概 11 倍。

編譯是一個考驗多核性能的操作,在我的黑蘋果上,編譯時可以看到 8 個 CPU 的負載都達到了 100%,因此在一定范圍內(nèi)(比如 10 核以內(nèi)),提升 CPU 核數(shù)遠比提升單核主頻對編譯速度的影響大。至于某些 20 核以上、單核性能較低的 CPU 編譯性能如何,希望有經(jīng)驗的讀者給予反饋。

優(yōu)化點總結(jié)

下表總結(jié)了文章中提到的各種優(yōu)化手段帶來的速度提升,參考原始時間均為 45 min(打包機器:13 寸? MacBook Pro):

方案序號優(yōu)化方案優(yōu)化后耗時 (min)時間減少百分比

1不常修改的文件二進制化2544.4%

2精簡指令集2740%

3關(guān)閉編譯優(yōu)化3815.6%

4使用 Mac Pro1566.7%

5虛擬磁盤426.7%

6公司現(xiàn)行方案(2+3+4+5)980%

7黑蘋果588.9%

8終極方案(1+2+3+5+7)4(預(yù)計)91.1%(預(yù)計)

嚴格意義上講,文章有點標題黨了,因為一句話來說就是:

能用硬件解決的問題,就不要用軟件解決。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容