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

[TOC]程序員

過慢的編譯速度有很是明顯的反作用。一方面,程序員在等待打包的過程當中可能會分心,好比刷刷朋友圈,看條新聞等等。這種認知上下文的切換會帶來不少隱形的時間浪費。另外一方面,大部分 app 都有本身的持續集成工具,若是打包速度太慢, 會影響整個團隊的開發進度。shell

所以,本文會分別討論平常開發和持續集成這兩種場景,分析打包速度慢的瓶頸所在,以及對應的解決方案。利用這些方案,筆者成功的把公司 app 的持續集成時間從 45 min 成功的減小到 9 min,效率提高高達 80%,理論上打包速度能夠提高 10 倍以上。若是用一句話總結就是:npm

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

平常開發

其實平常開發的優化空間並不大,由於默認狀況下 Xcode 會使用上次編譯時留下的緩存,也就是所謂的增量編譯。所以,平常開發的主要耗時由三部分構成:緩存

總耗時 = 增量編譯 + 連接 + 生成調試信息(dSYM)性能優化

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

  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.m
Compiling B.m
Compiling ...
Compiling N.m複製代碼

到了這一步,終於能夠作最關鍵的計算了,咱們能夠經過設置定時器,計算相鄰兩行輸出之間的間隔,這個間隔就是文件的編譯時間。固然,也有相似的輔助工具作好了這個邏輯:

npm install gnomon複製代碼

簡單的作一下排序,就能夠看到最耗時的前 200 個文件了,還能夠針對文件後綴做區分,計算總耗時等等。通過排查,咱們發現一半的編譯時間都花在了編譯 protobuf 文件上。

工程設置

除了針對超長耗時的文件進行 case-by-case 的分析外,另外一種方案是調整工程設置。通常來講,咱們的持續集成工具主要是用來給產品經理或者測試人員使用,用來體驗功能或者驗證 Bug,除非是須要上架 App Store,不然並不須要關心運行時性能。然而在手機上使用的 Release 模式,默認會開啓各類優化,這些優化都是犧牲編譯性能,換取運行時速度,對於上架的包而言無可厚非,但對於那些 Daily Build 包來講,就顯得得不償失了。

所以,加速打包的思路和優化的思路是徹底互逆的,咱們要作的就是關閉一切可能的優化。這裏推薦一篇文章:關於Xcode編譯性能優化的研究工做總結,能夠說至關全面了。

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

  1. 僅支持 armv7 指令集。手機上的指令集都屬於 ARM 系列,從老到新依次是 armv七、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%(預計)

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

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

相關文章
相關標籤/搜索