優化 iOS 專案的構建時間(二)

貝聊科技發表於2019-03-04

作者介紹:鍾子豪,貝聊科技高階 iOS 工程師

前言

之前一篇介紹 CCache 的文章探討了如何使用 CCache 來優化應用構建的時間,評論裡面收到了不少朋友反饋在使用的過程遇到了困難,最後無法成功應用上 CCache。其中的絕大部分問題我們在貝聊專案的整合過程中也遇到過,本文主要針對這些問題給出相應的解決方案,並從其他方面給出一些優化應用構建時間的建議。

提升快取命中率

通過命令 ccache -s 可以檢視 CCache 的整體快取命中率。要使 CCache 真正能減少編譯時間,命中率大約要達到 90% 。前文已經提過,頻繁地快取 miss 會比不使用 CCache 還慢,因此在整合 CCache 後需要保證90%的快取命中率,才能確實地提高構建速度,如果發現快取命中率過低,則需要分析日誌排查原因。

移除 Precompiled Prefix Header

使用了 PCH 檔案是最常見的導致快取命中率過低的原因。很多專案都會為了方便,把一些常用的類在 PCH 中 import 一次,而在編譯時 PCH 的內容會附加在每個檔案之前,PCH 內容的改變會導致整個編譯物件的全部檔案的內容改變,也就導致全部 CCache 快取失效。如果你遇到過明明只修改了一兩個原始檔,執行專案卻會全量編譯的情況,看看是不是PCH裡面import了太多依賴,如果暫時無法移除 PCH 則儘量減少裡面 import 的檔案。

以檔案內容作為編譯快取的 Key

因為常見的 git 切換分支,pod update等操作都會造成大量檔案的最後編輯時間變化,如果用檔案最後編輯時間作為快取的 key,經常會看見切換 git 分支或者 pod update 之後大量檔案快取失效。 CCache 預設就是用檔案內容的 MD4 摘要值來作為快取 key 的一部分的,只要不加上 file_stat_matches 選項即可。雖然計算並對比檔案內容的摘要值比簡單對比檔案的修改時間和大小要耗費更長的時間,但經過實踐發現使用檔案內容來作為 key 會有更穩定的編譯優化效果,以下是我目前在用的效果比較好的 CCache 配置:

#!/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,pch_defines
  
  exec ccache /usr/bin/clang "$@"
else
  exec clang "$@"
fi
複製程式碼

只在 Release 構建啟用 CCache

在使用 Debug 配置開發期間,Xcode 本身自帶增量編譯,只是在使用 Release 配置構建 AdHoc 或者 AppStore 的時候不能使用增量編譯。因此在 Debug 模式下啟用 CCache 其實意義不大,開發時檔案頻繁變更導致快取命中率低,反而會拖慢日常開發節奏。

對單個 Pod 不啟用 CCache

使用 CCache 需要關閉Clang Module功能,而有些第三方庫因為使用了@import語法導致如果關閉 Clang Module 則無法使用@import 語法繼而導致編譯出錯。這種情況可以單獨設定一個 Pod 不要啟用 CCache 即可。

下面的 Podfile 配置程式碼同時演示瞭如何只在 Release 構建啟用 CCache 以及對單個 Pod 不使用 CCache:

# Podfile

target 'YourApp' do # 替換為你的 target 名
  post_install do |installer_representation|
    installer_representation.pods_project.targets.each do |target|
      if config.name != 'Debug' && target.name != 'SomePod' # 替換為你想要排除的 Pod 的名字
        config.build_settings['CC'] = '$(PODS_ROOT)/ccache-clang' # 替換為你的 ccache-clang 檔案路徑
      end
    end
  end
end
  
複製程式碼

其他的優化點

這些優化點與 CCache 無關,但我嘗試過之後發現對編譯速度和開發體驗有一定提升,因此一併列出。

調整編譯的最大併發數

更新:Xcode 9.3 釋出後,我又用同樣的專案驗證了一次,得出了近似的結果,但差異較小,建議先進行 Benchmark 再決定是否要應用到自己專案中。

我之前通過這條命令

defaults write com.apple.dt.Xcode IDEBuildOperationMaxNumberOfConcurrentCompileTasks `sysctl -n hw.ncpu`
複製程式碼

把編譯時的最大併發數設定為 CPU 的核心數,沒想到這竟然不是最好的配置。我最早是從下面這篇文章發現的: The best hardware to build with Swift is not what you might think | 領英

雖然原文作者說在 Xcode 9 已經修復了這個問題,但我還是用一個純 Objective-C 的專案做實驗驗證,結果如下:

編譯併發數 編譯三次平均耗時(秒)
4 118.3
6 110.6
8 126.5

實驗結果也是讓我很意外,沒想到 8 執行緒的編譯速度居然還不如 4 執行緒,雖然差距不算大,但通過調整編譯併發數對編譯速度的確有影響,大家可以在自己的構建機器上用自己的專案實驗一下,找到最優的配置。

另外我順便用同一個專案測試了一下 Xcode 9 的 New Build System 的效能,結果如下

編譯併發數 編譯三次平均耗時(秒)
4 132.3
6 133.7
8 134.5

發現 New Build System 的編譯速度略遜於舊的編譯系統,但編譯過程更加嚴謹,例如在 Copy Bundle Resources 步驟時,如果發現缺少了某個圖片資原始檔會作為錯誤而不是警告丟擲。如果你的專案已經切換到了 New Build System,我建議繼續使用,畢竟未來蘋果會針對其做大量優化,這一點效能問題應該會得到解決。

避免標頭檔案的遞迴查詢

在排查編譯時間過長的元凶時,發現在專案的 Build Settings-> Header Search Paths 中居然有一行 ${PROJECT_DIR}/**,可能是以前某個同事遇到找不到標頭檔案編譯錯誤,沒有細想就加進去的。這項配置會導致編譯檔案時在整個專案目錄中遞迴查詢標頭檔案,顯著地增加查詢標頭檔案的時間,單個檔案的編譯時間可能會增加2~3倍之多。

清理廢棄程式碼

沒有程式碼的編譯速度能快得過「沒有程式碼」,這是很顯而易見的優化手段,不過實現起來可能工作量較大。在公司業務快速迭代和更新時很容易就會產生一些不再使用的模組,定期清理既能減少專案的構建時間也能減小安裝包的體積,一舉兩得。如果覺得這些程式碼以後還會用得上,可以只移除 Xcode 專案的引用而保留下來檔案以作參考。

優化 iOS 專案的構建時間(二)

在 Release 配置下關閉警告資訊

clang 在編譯的過程中如果遇到不規範的程式碼,例如有未使用的變數,會生成一段警告資訊並輸出到控制檯或者日誌,處理這些警告資訊也是需要佔用一些的計算資源的。警告資訊建議在開發階段就處理掉,如果警告資訊是來自 CocoaPods 整合的第三方庫的話,可以在 Podfile 中把inhibit_all_warnings! 選項開啟。貝聊的專案通過這個操作把編譯時間減少了約 40 秒。

優化 iOS 專案的構建時間(二)

總結

我們的專案通過上面這些手段,最終把平均的編譯時間從 1300 多秒減少到 300 秒內,視乎 CCache 的命中率會有所浮動,但總體上是大幅減少,希望這些經驗能對你的專案有所幫助。

相關文章