iOS 項目的編譯速度提升

咱們的項目使用 CocoaPods 來管理第三方庫和私有庫的依賴,對大部分項目來講應該是標配了。目前仍是純 Objective-C 的項目,沒有引入 Swift。html

調研過的方案git

下面列出我研究過的一些主流方案以及我最後沒有采用的緣由,這些方案有各自的侷限性,可是也給了我很多啓發,思考過程跟最終方案同樣有價值。程序員

cocoapods-packagergithub

cocoapods-packager 能夠將任意的 pod 打包成 Static Library,省去重複編譯的時間,必定程度上能夠加快編譯時間,可是也有自身的缺點:緩存

  1. 優化不完全,只能優化第三方和私有 Pod 的編譯速度,對於其餘改動頻繁的業務代碼無能爲力服務器

  2. 私有庫和第三方庫的後續更新很麻煩,當有源碼修改後,須要從新打包上傳到內部的 Git 倉庫框架

  3. 過多的二進制文件會拖慢 Git 的操做速度(目前還沒部署 Git 的 LFS分佈式

  4. 難以調試源碼ide

Carthage工具

這個方案跟 cocoapods-packager 比較相似,優缺點都差很少,但 Carthage 能夠比較方便地調試源碼。由於咱們目前已經大規模使用 CocoaPods,轉用 Carthage 來作包管理須要作大量的轉換工做,因此不考慮這個方案了。

Buck

Buck 是一套通用的構建系統,由 Facebook 開源。最大的特點是智能的增量編譯能夠極大地提升構建速度。最先據說 Buck 的時候,它還只能用在安卓上,如今已經適配了 iOS。

它能增快構建速度的主要緣由是緩存了編譯結果,經過持續監視項目目錄的文件變化,每次編譯時只編譯有改動的文件。另一個讓我很受啓發的功能是 HTTP Cache Server,經過一臺緩存文件服務器來保存你們的編譯結果,這樣只要團隊裏其中一人編譯過的文件,其餘人就不用再編譯了,直接下載就行。

Buck 是個至關完備的解決方案,不少國外的大公司例如 Uber 都已經用上。我也花了不少時間來研究,最終仍是認爲對咱們的項目和團隊來講,目前並非很適合,主要緣由是:

  1. Buck 拋棄了 Xcode 的項目文件,須要手工編寫配置文件來指定編譯規則,這要對現有項目做出大幅度的調整。咱們目前還在快速迭代新功能,沒有餘暇和人手來實施。

  2. 開發和調試的流程都得作出很大的改變。由於 Buck 接管了項目編譯的過程,想調試項目不能簡單地在 Xcode 裏面 ?+R 了,得先反過來讓 Buck 生成 Xcode 的項目文件。Uber 的工程師甚至推薦使用 Nuclide 來代替 Xcode 做爲開發環境。雖然原理上是可行的,可是團隊須要花很多時間來適應,短時間內效率下降無可避免。

  3. 用 Xcode 調試代碼享受不到加快編譯速度的好處。雖然能夠用 buck 命令啓動 App,而後在命令行裏啓動 lldb 來調試,但那就沒法使用 Xcode 的調試工具 例如 View Debugging 和 Memory Graph Debugger。

Bazel

Bazel 跟 Buck 很類似,是 Google 開源的,優缺點跟 Buck 都差很少,再也不詳細說了。

distcc 分佈式編譯

原理是把一部分須要編譯的文件發送到服務器上,服務器編譯完成後把編譯產物傳回來。我嘗試了一下比較出名的 distcc,搭建過程比較簡單,最後也能成功地把編譯任務分派到內網的多臺服務器上。可是其餘編譯服務器的 CPU 佔用老是很低,只有 20% 左右;也就是說分派任務的速度甚至還趕不上服務器編譯的速度,分派任務而後回傳編譯產物這個過程所耗費的時間超過了本地直接編譯。不停調整參數反覆試驗了不少次,最後發現編譯時間徹底沒有變快,甚至還有點變慢了。可能以咱們目前項目的規模並不適合使用分佈式編譯。

最終方案:CCache

先來看看我對於解決方案的訴求:

  1. 能大幅度地提高編譯速度,起碼要減小掉 50% 的編譯時間

  2. 不須要對項目做出重大調整

  3. 不須要改變開發工具鏈

CCache 是一個可以把編譯的中間產物緩存起來的工具,在其餘領域已經有很多應用,只是在 iOS 界的實踐比較少。通過個人實踐,它可以知足我前面的三點要求。我最先認識到它是搜到了這篇文章:Using ccache for Fun and Profit | Inside PSPDFKit

若是你不使用 CocoaPods,參照上面的文章便可。由於針對 CocoaPods 須要做出一些額外的調整,因此仍是說明一下。下面就來講說要怎樣把 CCache 應用在用 CocoaPods 做爲包管理工具的 iOS 項目中。

安裝步驟:

注意:項目路徑不能有中文,不然會影響 CCache 的正常工做

安裝 CCache

首先你須要在電腦上安裝 Homebrew,對使用 macOS 的程序員來講應該是標配,略過。

經過 Homebrew 安裝 CCache, 在命令行中執行

$ brew install ccache

命令跑完後即安裝成功。

建立 CCache 編譯腳本

爲了能讓 CCache 介入到整個編譯的過程,咱們要把 CCache 做爲項目的 C 編譯器,當 CCache 找不到編譯緩存時,它會再把編譯指令傳遞給真正的編譯器 clang。

新建一個文件命名爲ccache-clang, 內容爲下面這段腳本,放到你的項目裏

ccache-clang

#!/bin/sh
if type -p ccache >/dev/null 2>&1; then
export CCACHE_MAXSIZE=10G
export CCACHE_CPP2=true
export CCACHE_HARDLINK=true
export CCACHE_SLOPPINESS=file_macro,time_macros,include_file_mtime,include_file_ctime,file_stat_matches
# 指定日誌文件路徑到桌面,等下排查集成問題有用,集成成功後刪除,不然很佔磁盤空間
export CCACHE_LOGFILE='~/Desktop/CCache.log'
exec ccache /usr/bin/clang "$@"
else
exec clang "$@"
fi

在命令行中,cd 到 ccache-clang 文件的目錄,把它的權限改爲可執行文件

$ chmod 777 ccache-clang

若是你的代碼或者是第三方庫的代碼用到了C++,則把ccache-clang這個文件複製一份,重命名成ccache-clang++。相應的對clang的調用也要改爲clang++,不然 CCache 不會應用在 C++ 的代碼上。

ccache-clang++

#!/bin/sh
if type -p ccache >/dev/null 2>&1; then
export CCACHE_MAXSIZE=10G
export CCACHE_CPP2=true
export CCACHE_HARDLINK=true
export CCACHE_SLOPPINESS=file_macro,time_macros,include_file_mtime,include_file_ctime,file_stat_matches
# 指定日誌文件路徑到桌面,等下排查集成問題有用,集成成功後刪除,不然很佔磁盤空間
export CCACHE_LOGFILE='~/Desktop/CCache.log'
exec ccache /usr/bin/clang++ "$@"
else
exec clang++ "$@"
fi

完成後項目中應該有這兩個文件

Xcode 項目的調整

定義CC常量

在你項目的構建設置(Build Settings)中,添加一個常量CC,這個值會讓 Xcode 在編譯時把執行路徑的可執行文件當作 C 編譯器。

v2-2d7e2c013a001290c3dea8fc13734a68_b.png

v2-b0844daa58151392b61d9d3f0fd33955_b.png

CC常量的值爲 $(SRCROOT)/ccache-clang,若是你的腳本不是放在項目根目錄,則自行調整路徑。若是一運行項目就報錯,檢查下路徑是否是填錯了。

關閉 Clang Modules

由於 CCache 不支持 Clang Modules,因此須要把 Enable Modules 的選項關掉。這個問題在 CocoaPods 上如何處理,後面會講。

v2-b41ba260e37803538a7108a30c20153b_b.png

關閉了 Enable Modules 後須要做出的調整

由於關閉了 Enable Modules,因此必須刪除全部的 @import語句,替換爲#import的語法

例如將 @import UIKit 替換爲 #import。以後,若是你用到了其餘的系統框架例如 AVFoundation、CoreLocation等,如今 Xcode 不會再幫你自動引入了,你得要在項目 Target 的 Build Phrase -> Link Binary With Libraries 裏面本身手動引入。

測試效果

嘗試編譯一遍,而後在命令行裏輸入 cache -s 就能看見相似下面的 ccache 運行狀況統計:

cache directory                     /Users/mac/.ccache

primary config                      /Users/mac/.ccache/ccache.conf

secondary config      (readonly)    /usr/local/Cellar/ccache/3.3.4_1/etc/ccache.conf

cache hit (direct)                 14378

cache hit (preprocessed)            1029

cache miss                          7875

cache hit rate                     66.18 %

called for link                       61

called for preprocessing              48

compile failed                         2

preprocessor error                     4

can't use precompiled header          70

unsupported compiler option         2332

no input file                         11

cleanups performed                     0

files in cache                     35495

cache size                           1.3 GB

max cache size                       5.0 GB

若是成功接入,就能看見 cache miss 不爲0。由於第一次編譯沒有緩存,確定是全 miss 的。接着編譯第二遍,若是能看見 cache hit 的數字開始飆升,恭喜你,接入成功了。

CocoaPods 的處理

若是你的項目不用 CocoaPods 來作包管理,那你已經徹底接入成功了,不用執行下面的操做。

由於 CocoaPods 會單獨把第三方庫打包成一個 Static Library(或者是Dynamic Framework,若是用了 use_frameworks!選項),因此 CocoaPods 生成的 Static Library 也須要把 Enable Modules 選項給關掉。可是由於 CocoaPods 每次執行 pod update 的時候都會把 Pods 項目從新生成一遍,若是直接在 Xcode 裏面修改 Pods 項目裏面的 Enable Modules 選項,下次執行pod update的時候又會被改回來。咱們須要在 Podfile 裏面加入下面的代碼,讓生成的項目關閉 Enable Modules 選項,同時加入 CC 參數,不然 pod 在編譯的時候就沒法使用 CCache 加速:

post_install do |installer_representation|
installer_representation.pods_project.targets.each do |target|
target.build_configurations.each do |config|
#關閉 Enable Modules
config.build_settings['CLANG_ENABLE_MODULES'] = 'NO'
# 在生成的 Pods 項目文件中加入 CC 參數,路徑的值根據你本身的項目來修改
config.build_settings['CC'] = '$(PODS_ROOT)/../ccache-clang'
end
end
end

須要注意的是,若是你使用的某個 Pod 引用了系統框架,例如AFNetworking引用了System Configuration,你須要在你本身項目的Build Phrase -> Link Binary With Libraries裏面代爲引入,不然你編譯時可能會收到 Undefined symbols xxx for architecture yyy一類的錯誤。有點回到了原始時代的感受,但考慮到編譯速度的極大提高,這一點代價能夠接受。

集成問題排查

重點關注日誌文件的輸出和ccache -s 命令的統計,若是在日誌中看到了 unsupported compiler option -fmodules 這樣的字眼,就是你的 Enable Modules 沒有關掉了,根據前面的步驟仔細檢查。其餘問題,參考官方文檔的 Troubleshooting

進一步的優化

移除 Precompiled Header File

PCH 的內容會被附加在每一個文件前面,而 CCache 是根據文件內容的 MD4 摘要來查找緩存的,所以當你修改了 PCH 或者 PCH 引用到的頭文件的內容時,會形成所有緩存失效,只能全體從新編譯。CCache 在首次編譯的時候由於須要更新緩存,會形成編譯時間變長,對貝聊的項目來講變長了差很少一倍。所以若是 PCH 或者 PCH 引入的文件被頻繁修改的話,緩存就會頻繁地 miss,這種狀況下還不如不用 CCache。

爲了不以上這種狀況,我建議在 PCH 裏面儘可能少引入頭文件,只保留比較少更改的系統框架和第三方類庫的頭文件。最好是把 PCH 完全刪除,反正蘋果如今也不建議使用 PCH 了,Xcode 新建的項目默認都是不帶 PCH 的。

在團隊內部共享緩存文件夾

這個優化方式我嘗試過,最終效果不是很好,所以沒有采用。CCache 的官方文檔中有一段關於共享緩存文件夾的說明,描述瞭如何修改 CCache 的配置,讓編譯緩存可以在多臺電腦之間公用,理論上只要其中一我的編譯過的文件其餘人就能直接下載到了,節約了整個團隊的時間。由於 Buck 也有相似的機制,我以爲值得嘗試一下,便在公司局域網內搭建了一個 OwnCloud 網盤,讓你們把本身電腦上的 CCache 緩存目錄放上去共享。雖然試驗是成功了,可是實際效果並很差。由於同步在多臺電腦上大小達到幾個G的緩存目錄,須要在後臺進行不少文件的對比和傳輸的工做,在編譯的同時進行這些操做會耗費很多計算資源,反而會拖慢編譯速度。加上移除掉 PCH 後,其實緩存的命中率已經至關可觀了,不太須要經過共享緩存來進一步提升緩存命中率,因此我最後放棄了共享緩存這個想法。若是你對緩存命中率仍是不滿意的話,能夠考慮往這個方向嘗試一下。

總結

經過集成 CCache,咱們的項目在 Xcode 裏面的打包(在菜單裏面選擇 Product -> Archive)時間從 11~12分鐘減小到了 130 秒,大概有五倍的提高,成果喜人。集成的過程其實很簡單,我從開始嘗試到集成成功總共就花了兩個小時。若是你也被過長的編譯時間困擾,建議嘗試一下。

相關文章
相關標籤/搜索