作者 子豪 貝聊iOS工程師
前言
貝聊目前開發(fā)的兩款A(yù)pp分別是貝聊家長版和貝聊老師版旗吁,最近因為在快速迭代開發(fā)新功能,項目規(guī)模急速增長,單個端業(yè)務(wù)代碼約23萬行认轨,私有庫約6萬行,第三方庫代碼約15萬行月培,單個客戶端的代碼行數(shù)約60萬∴易郑現(xiàn)在打包一次耗時需要11~12分鐘。雖然還遠遠比不上 Facebook 的40分鐘杉畜,但是我們在內(nèi)測的時候纪蜒,經(jīng)常一天要發(fā)布內(nèi)測版兩到三次。打包時CPU占用基本上是百分百的此叠,因為沒有專門的 CI 機器霍掺,對負責(zé)打包的同事(其實就是我自己)的工作時間占用比較多,所以最近一直在尋找加快打包速度的方案拌蜘。
目前的項目架構(gòu)
我們的項目使用 CocoaPods 來管理第三方庫和私有庫的依賴杆烁,對大部分項目來說應(yīng)該是標配了。目前還是純 Objective-C 的項目简卧,沒有引入 Swift兔魂。
調(diào)研過的方案
下面列出我研究過的一些主流方案以及我最后沒有采用的原因,這些方案有各自的局限性举娩,但是也給了我不少啟發(fā)析校,思考過程跟最終方案一樣有價值。
cocoapods-packager
cocoapods-packager 可以將任意的 pod 打包成 Static Library铜涉,省去重復(fù)編譯的時間智玻,一定程度上可以加快編譯時間,但是也有自身的缺點:
- 優(yōu)化不徹底芙代,只能優(yōu)化第三方和私有 Pod 的編譯速度吊奢,對于其他改動頻繁的業(yè)務(wù)代碼無能為力
- 私有庫和第三方庫的后續(xù)更新很麻煩,當有源碼修改后纹烹,需要重新打包上傳到內(nèi)部的 Git 倉庫
- 過多的二進制文件會拖慢 Git 的操作速度(目前還沒部署 Git 的 LFS)
- 難以調(diào)試源碼
Carthage
這個方案跟 cocoapods-packager 比較類似页滚,優(yōu)缺點都差不多,但 Carthage 可以比較方便地調(diào)試源碼铺呵。因為我們目前已經(jīng)大規(guī)模使用 CocoaPods裹驰,轉(zhuǎn)用 Carthage 來做包管理需要做大量的轉(zhuǎn)換工作,所以不考慮這個方案了片挂。
Buck
Buck 是一套通用的構(gòu)建系統(tǒng)幻林,由 Facebook 開源贞盯。最大的特色是智能的增量編譯可以極大地提高構(gòu)建速度。最早聽說 Buck 的時候沪饺,它還只能用在安卓上邻悬,現(xiàn)在已經(jīng)適配了 iOS。
它能增快構(gòu)建速度的主要原因是緩存了編譯結(jié)果随闽,通過持續(xù)監(jiān)視項目目錄的文件變化父丰,每次編譯時只編譯有改動的文件。另外一個讓我很受啟發(fā)的功能是 HTTP Cache Server掘宪,通過一臺緩存文件服務(wù)器來保存大家的編譯結(jié)果蛾扇,這樣只要團隊里其中一人編譯過的文件,其他人就不用再編譯了魏滚,直接下載就行镀首。
Buck 是個相當完備的解決方案,很多國外的大公司例如 Uber 都已經(jīng)用上鼠次。我也花了很多時間來研究更哄,最終還是認為對我們的項目和團隊來說,目前并不是很適合腥寇,主要原因是:
- Buck 拋棄了 Xcode 的項目文件成翩,需要手工編寫配置文件來指定編譯規(guī)則,這要對現(xiàn)有項目作出大幅度的調(diào)整赦役。我們目前還在快速迭代新功能麻敌,沒有余暇和人手來實施。
- 開發(fā)和調(diào)試的流程都得做出很大的改變掂摔。因為 Buck 接管了項目編譯的過程术羔,想調(diào)試項目不能簡單地在 Xcode 里面 ?+R 了,得先反過來讓 Buck 生成 Xcode 的項目文件乙漓。Uber 的工程師甚至推薦使用 Nuclide 來代替 Xcode 作為開發(fā)環(huán)境级历。雖然原理上是可行的,但是團隊需要花不少時間來適應(yīng)叭披,短期內(nèi)效率降低無可避免寥殖。
- 用 Xcode 調(diào)試代碼享受不到加快編譯速度的好處。雖然可以用 buck 命令啟動 App趋观,然后在命令行里啟動 lldb 來調(diào)試扛禽,但那就無法使用 Xcode 的調(diào)試工具 例如 View Debugging 和 Memory Graph Debugger锋边。
Bazel
Bazel 跟 Buck 很相似皱坛,是 Google 開源的,優(yōu)缺點跟 Buck 都差不多豆巨,不再詳細說了剩辟。
distcc 分布式編譯
原理是把一部分需要編譯的文件發(fā)送到服務(wù)器上,服務(wù)器編譯完成后把編譯產(chǎn)物傳回來。我嘗試了一下比較出名的 distcc
贩猎,搭建過程比較簡單熊户,最后也能成功地把編譯任務(wù)分派到內(nèi)網(wǎng)的多臺服務(wù)器上。但是其他編譯服務(wù)器的 CPU 占用總是很低吭服,只有 20% 左右嚷堡;也就是說分派任務(wù)的速度甚至還趕不上服務(wù)器編譯的速度,分派任務(wù)然后回傳編譯產(chǎn)物這個過程所耗費的時間超過了本地直接編譯艇棕。不停調(diào)整參數(shù)反復(fù)試驗了很多次蝌戒,最后發(fā)現(xiàn)編譯時間完全沒有變快,甚至還有點變慢了沼琉”惫叮可能以我們目前項目的規(guī)模并不適合使用分布式編譯。
最終方案:CCache
先來看看我對于解決方案的訴求:
- 能大幅度地提升編譯速度打瘪,起碼要減少掉 50% 的編譯時間
- 不需要對項目作出重大調(diào)整
- 不需要改變開發(fā)工具鏈
CCache 是一個能夠把編譯的中間產(chǎn)物緩存起來的工具友鼻,在其他領(lǐng)域已經(jīng)有不少應(yīng)用,只是在 iOS 界的實踐比較少闺骚。經(jīng)過我的實踐彩扔,它能夠滿足我前面的三點要求。我最早認識到它是搜到了這篇文章:https://pspdfkit.com/blog/2015/ccache-for-fun-and-profit/
如果你不使用 CocoaPods僻爽,參照上面的文章即可借杰。因為針對 CocoaPods 需要作出一些額外的調(diào)整,所以還是說明一下进泼。下面就來說說要怎樣把 CCache 應(yīng)用在用 CocoaPods 作為包管理工具的 iOS 項目中蔗衡。
安裝步驟:
注意:項目路徑不能有中文,否則會影響 CCache 的正常工作
安裝 CCache
首先你需要在電腦上安裝 Homebrew乳绕,對使用 macOS 的程序員來說應(yīng)該是標配绞惦,略過。
通過 Homebrew 安裝 CCache洋措, 在命令行中執(zhí)行
$ brew install ccache
命令跑完后即安裝成功济蝉。
創(chuàng)建 CCache 編譯腳本
為了能讓 CCache 介入到整個編譯的過程,我們要把 CCache 作為項目的 C 編譯器菠发,當 CCache 找不到編譯緩存時王滤,它會再把編譯指令傳遞給真正的編譯器 clang。
新建一個文件命名為ccache-clang
, 內(nèi)容為下面這段腳本滓鸠,放到你的項目里
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 文件的目錄曲饱,把它的權(quán)限改成可執(zhí)行文件
$ chmod 777 ccache-clang
如果你的代碼或者是第三方庫的代碼用到了C++,則把ccache-clang
這個文件復(fù)制一份珠月,重命名成ccache-clang++
扩淀。相應(yīng)的對clang
的調(diào)用也要改成clang++
,否則 CCache 不會應(yīng)用在 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
完成后項目中應(yīng)該有這兩個文件
Xcode 項目的調(diào)整
定義CC常量
在你項目的構(gòu)建設(shè)置(Build Settings)中旺韭,添加一個常量CC
,這個值會讓 Xcode 在編譯時把執(zhí)行路徑的可執(zhí)行文件當做 C 編譯器掏觉。
CC常量的值為 $(SRCROOT)/ccache-clang
区端,如果你的腳本不是放在項目根目錄,則自行調(diào)整路徑澳腹。如果一運行項目就報錯织盼,檢查下路徑是不是填錯了。
關(guān)閉 Clang Modules
因為 CCache 不支持 Clang Modules酱塔,所以需要把 Enable Modules 的選項關(guān)掉沥邻。這個問題在 CocoaPods 上如何處理,后面會講羊娃。
關(guān)閉了 Enable Modules 后需要作出的調(diào)整
因為關(guān)閉了 Enable Modules唐全,所以必須刪除所有的 @import
語句,替換為#import
的語法
例如將 @import UIKit
替換為 #import <UIKit/UIKit.h>
蕊玷。之后邮利,如果你用到了其他的系統(tǒng)框架例如 AVFoundation、CoreLocation等垃帅,現(xiàn)在 Xcode 不會再幫你自動引入了延届,你得要在項目 Target 的 Build Phrase -> Link Binary With Libraries 里面自己手動引入。
測試效果
嘗試編譯一遍贸诚,然后在命令行里輸入 ccache -s
就能看見類似下面的 ccache 運行情況統(tǒng)計:
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 的數(shù)字開始飆升运悲,恭喜你龄减,接入成功了。
CocoaPods 的 處理
如果你的項目不用 CocoaPods 來做包管理扇苞,那你已經(jīng)完全接入成功了欺殿,不用執(zhí)行下面的操作寄纵。
因為 CocoaPods 會單獨把第三方庫打包成一個 Static Library(或者是Dynamic Framework鳖敷,如果用了 use_frameworks!
選項)脖苏,所以 CocoaPods 生成的 Static Library 也需要把 Enable Modules 選項給關(guān)掉。但是因為 CocoaPods 每次執(zhí)行 pod update
的時候都會把 Pods 項目重新生成一遍定踱,如果直接在 Xcode 里面修改 Pods
項目里面的 Enable Modules 選項棍潘,下次執(zhí)行pod update
的時候又會被改回來。我們需要在 Podfile 里面加入下面的代碼崖媚,讓生成的項目關(guān)閉 Enable Modules 選項亦歉,同時加入 CC 參數(shù),否則 pod 在編譯的時候就無法使用 CCache 加速:
post_install do |installer_representation|
installer_representation.pods_project.targets.each do |target|
target.build_configurations.each do |config|
#關(guān)閉 Enable Modules
config.build_settings['CLANG_ENABLE_MODULES'] = 'NO'
# 在生成的 Pods 項目文件中加入 CC 參數(shù)畅哑,路徑的值根據(jù)你自己的項目來修改
config.build_settings['CC'] = '$(PODS_ROOT)/../ccache-clang'
end
end
end
需要注意的是肴楷,如果你使用的某個 Pod 引用了系統(tǒng)框架,例如AFNetworking
引用了System Configuration
荠呐,你需要在你自己項目的Build Phrase -> Link Binary With Libraries
里面代為引入赛蔫,否則你編譯時可能會收到 Undefined symbols xxx for architecture yyy
一類的錯誤。有點回到了原始時代的感覺泥张,但考慮到編譯速度的極大提升呵恢,這一點代價可以接受。
集成問題排查
重點關(guān)注日志文件的輸出和ccache -s
命令的統(tǒng)計媚创,如果在日志中看到了 unsupported compiler option -fmodules
這樣的字眼渗钉,就是你的 Enable Modules 沒有關(guān)掉了,根據(jù)前面的步驟仔細檢查钞钙。其他問題鳄橘,參考官方文檔的 Troubleshooting。
進一步的優(yōu)化
移除 Precompiled Header File
PCH 的內(nèi)容會被附加在每個文件前面芒炼,而 CCache 是根據(jù)文件內(nèi)容的 MD4 摘要來查找緩存的挥唠,因此當你修改了 PCH 或者 PCH 引用到的頭文件的內(nèi)容時,會造成全部緩存失效焕议,只能全體重新編譯宝磨。CCache 在首次編譯的時候因為需要更新緩存,會造成編譯時間變長盅安,對貝聊的項目來說變長了差不多一倍唤锉。因此如果 PCH 或者 PCH 引入的文件被頻繁修改的話,緩存就會頻繁地 miss别瞭,這種情況下還不如不用 CCache窿祥。
為了避免以上這種情況,我建議在 PCH 里面盡量少引入頭文件蝙寨,只保留比較少更改的系統(tǒng)框架和第三方類庫的頭文件晒衩。最好是把 PCH 徹底刪除嗤瞎,反正蘋果現(xiàn)在也不建議使用 PCH 了,Xcode 新建的項目默認都是不帶 PCH 的听系。
在團隊內(nèi)部共享緩存文件夾
這個優(yōu)化方式我嘗試過贝奇,最終效果不是很好,因此沒有采用靠胜。CCache 的官方文檔中有一段關(guān)于共享緩存文件夾的說明掉瞳,描述了如何修改 CCache 的配置,讓編譯緩存能夠在多臺電腦之間公用浪漠,理論上只要其中一個人編譯過的文件其他人就能直接下載到了陕习,節(jié)約了整個團隊的時間。因為 Buck 也有類似的機制址愿,我覺得值得嘗試一下该镣,便在公司局域網(wǎng)內(nèi)搭建了一個 OwnCloud 網(wǎng)盤,讓大家把自己電腦上的 CCache 緩存目錄放上去共享响谓。雖然試驗是成功了损合,但是實際效果并不好。因為同步在多臺電腦上大小達到幾個G的緩存目錄歌粥,需要在后臺進行很多文件的對比和傳輸?shù)墓ぷ魉觯诰幾g的同時進行這些操作會耗費不少計算資源,反而會拖慢編譯速度失驶。加上移除掉 PCH 后土居,其實緩存的命中率已經(jīng)相當可觀了,不太需要通過共享緩存來進一步提高緩存命中率嬉探,所以我最后放棄了共享緩存這個想法擦耀。如果你對緩存命中率還是不滿意的話,可以考慮往這個方向嘗試一下涩堤。
總結(jié)
通過集成 CCache眷蜓,我們的項目在 Xcode 里面的打包(在菜單里面選擇 Product -> Archive)時間從 11~12分鐘減少到了 130 秒,大概有五倍的提升胎围,成果喜人吁系。集成的過程其實很簡單,我從開始嘗試到集成成功總共就花了兩個小時白魂。如果你也被過長的編譯時間困擾汽纤,建議嘗試一下。
文章同步發(fā)布在 https://zhuanlan.zhihu.com/p/27584726