如何將 iOS 工程打包速度提升十倍以上

來源:互聯網
上載者:User

標籤:table   png   分布式   刪除   使用   note   重複   提高   介紹   

如何將 iOS 工程打包速度提升十倍以上 

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

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

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

日常開發

其實日常開發的最佳化空間並不大,因為預設情況下 Xcode 會使用上次編譯時間留下的緩衝,也就是所謂的增量編譯。因此,日常開發的主要耗時由三部分構成:

總耗時 = 增量編譯 + 連結 + 產生調試資訊(dSYM)

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

  1. 增量編譯: 因為耗時較短(大概十幾秒或者更少),幾乎不存在最佳化的空間,但是非常容易惡化。因為只有標頭檔不變的編譯單元才能被緩衝,如果某個檔案被 N 個檔案引用,且這個檔案的標頭檔發生了變化,那麼這 N 個檔案都會重編譯。APP 的分層架構一般都會做,但一個典型的誤區是在基礎庫的標頭檔中使用宏定義,比如定義一些全域都可以讀取的常量,比如是否開啟調試,伺服器的地址等等。這些常量一旦改變(比如為了調試或者切換到某些分支)就會導致應用重編譯。
  2. 連結:連結沒有緩衝,而且只能用單核進行,因此它的耗時主要取決於單核效能和磁碟讀寫速度。考慮到我們的目標檔案一般都比較小,因此 4K 隨機讀寫的效能應該會更重要一些。
  3. 調試資訊:日常開發時,並不需要產生 dSYM 檔案,這個檔案主要用於崩潰時尋找調用棧,方便線上應用進行調試,而開發過程中的崩潰可以直接在 Xcode 中看到,關閉這個功能 不會對開發產生任何負面影響。

日常開發的最佳化空間不大,即使是龐大的項目,落後的機器效能,關閉 dSYM 以後也就耗時 30s 左右。相比之下,打包速度可以最佳化和討論的地方就比較多了。

持續整合

在利用 Jenkins 等工具進行持續整合時,緩衝不推薦被使用。這是因為蘋果的緩衝不夠穩定,在某些情況下還存在 bug。比如明明本地已經修複了 bug,可以編譯通過,但上次的編譯緩衝沒有被正確清理,導致在打包機器上依然無法編譯通過。或者本地明明寫出了 bug,但同樣由於緩衝問題,打包機器依然可以編譯通過。

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

使用 CCache 緩衝

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

然而,CCache 最致命的問題是不支援 PCH 檔案和 Clang modules。PCH 的本意是最佳化編譯時間,我們假設有一個標頭檔 A 依賴了 M 個標頭檔,其中每個被依賴的標頭檔又依賴了 N 個 標頭檔,如所示:

由於 #import 的本質就是把被依賴標頭檔的內容拷貝到自己的標頭檔中來,因此標頭檔 A 中實際上包含了 M * N 個標頭檔的內容,也就需要 M * N 次檔案 IO 和相關處理。當項目中每增加一個依賴標頭檔 A 的檔案,就會重複一次上述的 M * N 複雜度的過程。

PCH 檔案的好處是,這個檔案中的標頭檔只會被編譯一次並緩衝下來,然後添加到項目中 所有 的標頭檔中去。上述問題倒是解決了,但很智障的一點是,所有檔案都會隱式的依賴所有 PCH 中的檔案,而真正需要被全域依賴的檔案其實非常少。因此實際開發中,更多的人會把 PCH 當成一種快速 import 的手段,而非編譯效能的最佳化。前文解釋過,PCH 檔案一旦發生修改,會導致徹徹底底,完完整整的項目重編譯,從而降低編譯速度。正是因為 PCH 的副作用甚至抵消了它帶來的最佳化,蘋果已經預設不使用 PCH 檔案了。

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

@import UIKit;

等價於

#import <UIKit/UIKit.h>

拋開自動連結 framework 這些小特性不談,Clang modules 可以理解為模組化的 PCH,它具備了 PCH 可以緩衝標頭檔的優點,同時提供了更細粒度的引用。

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

distcc

distcc 是一種分布式編譯工具,可以把需要被編譯的檔案發送到其他機器上編譯,然後接收編譯產物。然而,經過貼吧、貝聊、手Q 等應用的多方實驗,發現並不適合 iOS 應用。它的原理是多個用戶端共同編譯,但是絕大多數檔案其實編譯時間非常短,並不值得通過網路來回傳送,這種方案應該只適合單個檔案體量非常大的項目。在我們的項目中,使用 distcc 大幅度 增加了打包時間,大約耗時 1 小時左右。

定位瓶頸

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

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

gem install xcpretty

通過 gem 安裝後,只要把 xcodebuild 的輸出結果通過管道傳給 xcpretty 即可:

xcodebuild -scheme Release ... | xcpretty

下面是官方文檔中的 Demo:

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

Compiling A.mCompiling B.mCompiling ...Compiling N.m

到了這一步,終於可以做最關鍵的計算了,我們可以通過設定定時器,計算相鄰兩行輸出之間的間隔,這個間隔就是檔案的編譯時間。當然,也有類似的協助工具輔助做好了這個邏輯:

npm install gnomon 

簡單的做一下排序,就可以看到最耗時的前 200 個檔案了,還可以針對檔案尾碼作區分,計算總耗時等等。經過排查,我們發現一半的編譯時間都花在了編譯 protobuf 檔案上。

工程設定

除了針對超長耗時的檔案進行 case-by-case 的分析外,另一種方案是調整工程設定。一般來說,我們的持續整合工具主要是用來給產品經理或者測試人員使用,用來體驗功能或者驗證 Bug,除非是需要上架 App Store,否則並不需要關心運行時效能。然而在手機上使用的 Release 模式,預設會開啟各種最佳化,這些最佳化都是犧牲編譯效能,換取運行時速度,對於上架的包而言無可厚非,但對於那些 Daily Build 包來說,就顯得得不償失了。

因此,加速打包的思路和最佳化的思路是完全互逆的,我們要做的就是關閉一切可能的最佳化。這裡推薦一篇文章:關於Xcode編譯效能最佳化的研究工作總結,可以說相當全面了。

經過對其中各個參數的尋找資料和嘗試關閉,按照提升速度的降序排列,簡單整理幾個:

  1. 僅支援 armv7 指令集。手機上的指令集都屬於 ARM 系列,從老到新依次是 armv7、armv7s 和 arm64。新的指令集可以相容舊的機型,但舊的機型不能相容新的指令集。預設情況下我們打出來的包會有 armv7 和 arm64 兩種指令集, 前者負責兜底,而對於支援 arm64 指令集的機型來說,使用最新的指令集可以獲得更好的效能。當然代價就是產生兩種指令集花費了更多時間。所以在急速打包模式下,我們只產生 armv7 這種最老的指令集,犧牲了運行時效能換取編譯速度。
  2. 關閉編譯最佳化。最佳化的基本原理是犧牲編譯時間效能,追求運行時效能。常見的最佳化有編譯時間刪除無用代碼,保留調試資訊,函數內聯等等。因此提升打包速度的秘訣就是反其道而行之,犧牲運行時效能來換取編譯時間效能。筆者做的兩個最主要的最佳化是把 Optimize level 改成 O0,表示不做任何最佳化。
  3. 使用虛擬磁碟。編譯過程中需要大量的磁碟 IO,這主要發生在 Derived Data 目錄下,因此如果記憶體足夠,可以考慮划出 4G 左右的記憶體,建一個虛擬磁碟,這樣將會把磁碟 IO 最佳化為 記憶體 IO,從而提高速度。由於打包機器每次都會重編譯,因此並不需要擔心重啟機器後緩衝丟失的問題。
  4. 不產生 dYSM 檔案,前文已經介紹過。
  5. 一些其他的選項,參考前面推薦的文章。

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

因此,一般來說 只要精簡指令集並關閉最佳化即可,有條件的機器可以使用虛擬磁碟,不建議再做其它修改。

二進位化

二進位化主要指的是利靜態庫代替源碼,避免編譯。前文已經介紹過如何分析檔案的耗時,因此二進位化的收益非常容易計算出來。由於團隊分工問題,筆者沒有什麼二進位化的經驗,一般來說這個最佳化比較適合基礎架構組去實施。

硬體加速

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

在我的黑蘋果(i7 7820x 8 核 16 線程,16G 記憶體,三星 PM 961 512G SSD,下文簡稱黑蘋果)上,即使不開啟任何最佳化,從零開始編譯也僅需 5min。如果將 protobuf 檔案二進位化,再配合一些工程設定的最佳化,我不敢想象需要花多長時間,預計在 4min 左右吧,速度提升了大概 11 倍。

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

最佳化點總結

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

方案序號 最佳化方案 最佳化後耗時 (min) 時間減少百分比
1 不常修改的檔案二進位化 25 44.4%
2 精簡指令集 27 40%
3 關閉編譯最佳化 38 15.6%
4 使用 Mac Pro 15 66.7%
5 虛擬磁碟 42 6.7%
6 公司現行方案(2+3+4+5) 9 80%
7 黑蘋果 5 88.9%
8 終極方案(1+2+3+5+7) 4(預計) 91.1%(預計)

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

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

如何將 iOS 工程打包速度提升十倍以上

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.