引言
本文旨在記錄一次使用 CCache 對 Xcode Build 時間做優化的過程,并簡單的描述一下用法,總結一下其他使用到的優化方案,詳細記錄過程中涉及到的一些對于個人來說重要的基礎知識點,以及實踐中遇到的問題,以及最后解決問題的辦法
背景
相信上面這張圖最能代表我做這次優化的背景了,Clean 之后的一次 Build 時間達到了10min,在沒辦法通過硬件解決的情況下,只好尋找通過軟件解決的途徑了;并且這只是我們平時開發過程遇到的背景,其實還有就是打包的時間也是巨長;因此,再這樣的情況下,我們不得不進行一些些優化
基礎知識
這里的基礎知識主要是為了為后面優化過程中的涉及到的問題做鋪墊
Build 過程
預處理
'#import 的展開,這一步做的事情便是告訴處理器將我們引入的各個.h文件插入到 #import 的位置
宏定義的替換,這個好理解,不用解釋
當然,預處理完了之后會進行詞法分析啥的,這里就不一一提到,下面也是,就說說主要的一些過程
- 編譯
編譯過程將用戶可識別的語言翻譯成一組處理器可識別的操作碼,通常翻譯成匯編語言
- 匯編
匯編器將可讀的匯編代碼轉換為機器代碼。它會創建一個目標對象文件,一般簡稱為 對象文件。也就是后綴是 .obj 或者 .o 目標文件
- 鏈接(各種需要的FrameWork,Foundation.framework等之類)
將目標文件和庫文件關聯起來生成可執行文件
@import && #import && PCH文件
import ,先說 #include,#include 做的事情其實就是簡單的復制粘貼,將目標.h文件中的內容一字不落地拷貝到當前文件中,并替換掉這句 #include,而 #import 做的事情和 #include 是一樣的,只不過 OC 為了避免重復引用可能帶來的編譯錯誤,比如B和C都引用了A,D又同時引用了B和C,這樣A中定義的東西就在D中被定義了兩次,重復了,而加入了 #import,從而保證每個頭文件只會被引用一次。
所以,#import 還是拷貝粘貼,這樣就帶來一個問題:當引用關系很復雜,或者一個頭文件被非常多的實現文件引用時,編譯時引用所占的代碼量就會大幅上升(因為被引用的頭文件在各個地方都被copy了一遍)
于是就出來 PCH (預編譯頭文件),將公用的頭文件放入預編譯頭文件中預先進行編譯,然后在真正編譯工程時再將預先編譯好的產物加入到所有待編譯的 Source 中去,來加快編譯速度。iOS 開發中 Supporting Files 組內的 .pch 文件就是一個預編譯頭文件
@import Apple在 LLVM5.0 引入了一個新的編譯符號 @import,使用@符號將告訴編譯器去使用 Modules 的引用形式,從而獲取好處,比如想引用 Mapbox,可以寫成
@import Mapbox;
在使用上,這將等價于以前的#import<Mapbox/Mapbox.h>
,但是將使用Modules的特性。
什么是 Moudles? Modules 相當于將框架進行了封裝,然后加入在實際編譯之時加入了一個用來存放已編譯添加過的 Modules 列表。如果在編譯的文件中引用到某個 Modules 的話,將首先在這個列表內查找,找到的話說明已經被加載過則直接使用已有的,如果沒有找到,則把引用的頭文件編譯后加入到這個表中。這樣被引用到的 Modules 只會被編譯一次
動態庫與靜態庫
這兩個東西都是編譯好的二進制文件。都是不用再編譯了的,用法不同而已。更多關于區別,可以看這里
分析問題
好了,鋪墊了一波簡單的基礎知識,可以開始分析問題了:
Build 時間太長,先來看一下 Build 的 log ,看一下到底哪些東西耽誤了時間
足足等了10多分鐘,一直看著屏幕,發現并不是哪個庫或者哪個具體的操作耗時很長,結論就是文件6000多個,文件多導致的編譯時間長!
解決問題
好了,初步判斷就是文件太多了,開始動手:
還記得之前在網上看到各種大佬通過設置一些 Build Settings,趕緊翻出來看看:
- 1.Optimization Level

這個是 Xcode Build Setting 里的一個參數,Optimization Level 是指編譯器的優化層度 它一共有以下幾個選項:
None: 編譯器不進行任何代碼優化
Fast: 編譯器進行小幅度代碼優化,同時消耗更多的內存
Faster: 會進行所有可用的優化選項而不用花費額外的時間和內存。該選項不會執行循環展開或者內嵌函數。該選項會在提升代碼性能的同時增加編譯時間
Fastest: 該選項會作出盡可能多的嘗試來提高編譯性能。但同時會和內嵌函數機制存在沖突。一般不建議使用該選項。
Fastest, Smallest: 編譯器會進行所有可用的優化而不顯著的增加運行空間。該選項是打包代碼的最優選項
Fastest, Aggressive Optimizations:尋求大家幫助????各種找都沒找到

所以說我們平時開發的時候可以選擇使用 None 來不給代碼執行優化,這樣既可以減少編譯時間,而你的release 版應該選擇 Fastest, Smalllest,這樣既能執行所有的優化而不增加代碼長度,又能使執行文件占用更少的內存。
-
2.Debug Information Format
這一項設置的是是否將調試信息加入到可執行文件中,改為 DWARF 后,如果程序崩潰,將無法輸出崩潰位置對應的函數堆棧,但由于 Debug 模式下可以在 Xcode 中查看調試信息,所以改為 DWARF 影響并不大。這一項更改完之后,可以大幅提升編譯速度。
其實 Debug Information Format 就是表示是否生成.dSYM文件,也就是符號表。如果為 DWARF 就表示不生成.dSYM文件。
如果在使用 instrument 的時候記得把這個打開,不然你看到的就不是一個個的你看得懂的函數調用棧,就像沒有進行符號化的崩潰日志
- 3.將Build Active Architecture Only 改為Yes
這一項設置的是是否僅編譯當前架構的版本,如果為 No,會編譯所有架構的版本。需要注意的是,此選項在Release 模式下必須為No,否則發布的 ipa 在部分設備上將不能運行
=========悲傷的事情就是,上述的設置,在優化之前早就設置好了,所以,繼續接著優化=========
反過來思考一下:咱們主要的問題是編譯的文件太多,于是目的就是減少文件的編譯不就好了:
- 4.對部分一些常用的工具打包成靜態庫,這樣這部分代碼就不用再編譯了
可能是個人能力原因,大致看了一下,零零散散的能打包的都打包了,仿佛這一步也不大行得通。
-
5.既然原因是文件太多,那就簡單點,直接清理不要的類唄
工具比較多,方法也多,可以借鑒微信的瘦身實踐的里面提到的清理無用類,使用otool和link map我大致實踐了一下,實在復雜。索性,立馬使用 AppCode 的 Inspect 功能對工程分析一波,結果感人:
無用類沒有!沒有!但是無用的#import倒是一大堆,那么問題就來了,稍微動一下頭文件的引入,有可能導致大面積的重新編譯,可以優化!!! -
6.清理無用圖片資源
通過查看 build 的log,會發現其實還有一部分時間在拷貝圖片資源以及一些別的文件,于是,清理無用的資源,是不是又可以瘦身,又可以減少build時間,這里使用的是LSUnusedResources
這里得到的結果就是,在默認的篩選條件下,找到了10M
上述減少資源優化基本都是體力活,可能需要持續的實踐,為了看到快速的效果,于是,準備實踐一下之前看到的 CCache
- 7.CCache 編譯緩存(終于可以扣住文章主題了??????)
什么是CCache:
CCache 是一個能夠把編譯的中間產物緩存起來的工具,它會在實際編譯之前先檢查緩存。
根據bestswifter的這篇文章其實有提到,在我們平時的開發環境中,Xcode其實自己會做增量編譯,也就是說默認會使用上次編譯留下的緩存,但是在進行持續集成的時候,緩存不被推薦使用,但這是因為蘋果的緩存不穩定,某些情況下依然有bug的原因。因此我們只能手動刪除 Derived Data 文件夾,還是調用 xcodebuild clean 命令,都會把緩存清空。或者直接使用 xcodebuild archive,會自動忽略緩存。每次都要全部重編譯,因此時間當然慢了哦。
那么,要是我們有一個把編譯緩存做的很好的東西,是不是就可以好很多了~~
接入 CCache 的教程參見 貝聊科技CCache,為方便閱讀,這里做搬運工作:
安裝CCache
通過 Homebrew 安裝 CCache, 在命令行中執行
$ brew install ccache
命令執行無異常便是安裝成功
創建 CCache 編譯腳本
為了能讓 CCache 介入到整個編譯的過程,我們要把 CCache 作為項目的 C 編譯器,當 CCache 找不到編譯緩存時,它會再把編譯指令傳遞給真正的編譯器 clang。
新建一個文件命名為 ccache-clang
$ touch 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++ 的代碼上。
#!/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 編譯器。

CC 常量的值為
$(SRCROOT)/ccache-clang
,如果你的腳本不是放在項目根目錄,則自行調整路徑。如果一運行項目就報錯,檢查下路徑是不是填錯了。
- 關閉 Clang Modules,這一步真的很惡心
因為 CCache 不支持 Clang Modules,所以需要把 Enable Modules 的選項關掉。這個問題在 CocoaPods 上如何處理,后面會講。

關閉了 Enable Modules 后需要作出的調整
因為關閉了 Enable Modules,所以必須刪除所有的 @import語句,替換為#import的語法例如將 @import UIKit 替換為 #import <UIKit/UIKit.h>。之后,如果你用到了其他的系統框架例如 AVFoundation、CoreLocation等,現在 Xcode 不會再幫你自動引入了,你得要在項目 Target 的 Build Phrase -> Link Binary With Libraries 里面自己手動引入。
- 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一類的錯誤。有點回到了原始時代的感覺,但考慮到編譯速度的極大提升,這一點代價可以接受。
好了,到目前為止,你可以開始 cmd+b
了,第一次會比較慢,第二次或者往后,你就會發現 cache hit 變大了,隨著它的變大,時間你會發現越來越少

好了,看看集成了 CCache 的效果!!!!!,你沒看錯,時間真的少了一半

你以為文章就到此截止了?不行,還有問題沒有解決:
- 不支持 PCH 文件 怎么辦?一定要移除么,可我真的不想移除
其實貝聊有提到,當你修改了PCH或者PCH引用的到的頭文件時,會造成緩存失效,只能全部重新編譯,所以,只要你不會頻繁的更改PCH文件的話,或者不改,其實問題都不大,還是可以接受的,起碼能享受到CCache帶來的快感
小結
本文旨在對 Build 優化過程做一個記錄,記錄在優化過程中遇到的一個知識點以及困惑,有些個人的理解也穿插在其中,如果個人理解有誤,還望探討指正