過(guò)慢的編譯速度有非常明顯的副作用。一方面双谆,程序員在等待打包的過(guò)程中可能會(huì)分心壳咕,比如刷刷朋友圈,看條新聞等等顽馋。這種認(rèn)知上下文的切換會(huì)帶來(lái)很多隱形的時(shí)間浪費(fèi)谓厘。另一方面,大部分 app 都有自己的持續(xù)集成工具寸谜,如果打包速度太慢竟稳, 會(huì)影響整個(gè)團(tuán)隊(duì)的開(kāi)發(fā)進(jìn)度。
因此熊痴,本文會(huì)分別討論日常開(kāi)發(fā)和持續(xù)集成這兩種場(chǎng)景他爸,分析打包速度慢的瓶頸所在,以及對(duì)應(yīng)的解決方案愁拭。利用這些方案讲逛,筆者成功的把公司 app 的持續(xù)集成時(shí)間從 45 min 成功的減少到 9 min,效率提升高達(dá) 80%岭埠,理論上打包速度可以提升 10 倍以上。如果用一句話總結(jié)就是:
在絕對(duì)的實(shí)力(硬件)面前蔚鸥,一切技巧(軟件)都是浮云
日常開(kāi)發(fā)
其實(shí)日常開(kāi)發(fā)的優(yōu)化空間并不大惜论,因?yàn)槟J(rèn)情況下 Xcode 會(huì)使用上次編譯時(shí)留下的緩存,也就是所謂的增量編譯止喷。因此馆类,日常開(kāi)發(fā)的主要耗時(shí)由三部分構(gòu)成:
總耗時(shí) = 增量編譯 + 鏈接 + 生成調(diào)試信息(dSYM)
這里的增量編譯耗時(shí)比較短,即使是在我 14 年高配的 MacBook Pro(4核心弹谁,8 線程乾巧,2.5GHz i7 4870HQ句喜,下文簡(jiǎn)稱(chēng) MBP) 上,也僅僅耗時(shí)十秒上下沟于。我們的應(yīng)用代碼量大約一百多萬(wàn)行咳胃,業(yè)內(nèi)超過(guò)這個(gè)量級(jí)的應(yīng)用應(yīng)該不多。鏈接和生成調(diào)試信息各花費(fèi)不到 20s旷太,因此一次增量的編譯的時(shí)間開(kāi)銷(xiāo)在半分鐘到一分鐘左右展懈,我們逐個(gè)分析:
增量編譯: 因?yàn)楹臅r(shí)較短(大概十幾秒或者更少),幾乎不存在優(yōu)化的空間供璧,但是非常容易惡化存崖。因?yàn)橹挥蓄^文件不變的編譯單元才能被緩存,如果某個(gè)文件被 N 個(gè)文件引用睡毒,且這個(gè)文件的頭文件發(fā)生了變化来惧,那么這 N 個(gè)文件都會(huì)重編譯。APP 的分層架構(gòu)一般都會(huì)做演顾,但一個(gè)典型的誤區(qū)是在基礎(chǔ)庫(kù)的頭文件中使用宏定義违寞,比如定義一些全局都可以讀取的常量,比如是否開(kāi)啟調(diào)試偶房,服務(wù)器的地址等等趁曼。這些常量一旦改變(比如為了調(diào)試或者切換到某些分支)就會(huì)導(dǎo)致應(yīng)用重編譯。
鏈接:鏈接沒(méi)有緩存棕洋,而且只能用單核進(jìn)行挡闰,因此它的耗時(shí)主要取決于單核性能和磁盤(pán)讀寫(xiě)速度£蹋考慮到我們的目標(biāo)文件一般都比較小摄悯,因此 4K 隨機(jī)讀寫(xiě)的性能應(yīng)該會(huì)更重要一些。
調(diào)試信息:日常開(kāi)發(fā)時(shí)愧捕,并不需要生成 dSYM 文件奢驯,這個(gè)文件主要用于崩潰時(shí)查找調(diào)用棧,方便線上應(yīng)用進(jìn)行調(diào)試次绘,而開(kāi)發(fā)過(guò)程中的崩潰可以直接在? Xcode 中看到瘪阁,關(guān)閉這個(gè)功能不會(huì)對(duì)開(kāi)發(fā)產(chǎn)生任何負(fù)面影響。
日常開(kāi)發(fā)的優(yōu)化空間不大邮偎,即使是龐大的項(xiàng)目管跺,落后的機(jī)器性能,關(guān)閉 dSYM 以后也就耗時(shí) 30s 左右禾进。相比之下豁跑,打包速度可以?xún)?yōu)化和討論的地方就比較多了。
持續(xù)集成
在利用 Jenkins 等工具進(jìn)行持續(xù)集成時(shí)泻云,緩存不推薦被使用艇拍。這是因?yàn)樘O(píng)果的緩存不夠穩(wěn)定狐蜕,在某些情況下還存在 bug。比如明明本地已經(jīng)修復(fù)了 bug卸夕,可以編譯通過(guò)层释,但上次的編譯緩存沒(méi)有被正確清理,導(dǎo)致在打包機(jī)器上依然無(wú)法編譯通過(guò)娇哆∨壤郏或者本地明明寫(xiě)出了 bug,但同樣由于緩存問(wèn)題碍讨,打包機(jī)器依然可以編譯通過(guò)治力。
因此,無(wú)論是手動(dòng)刪除Derived Data文件夾勃黍,還是調(diào)用xcodebuild clean命令宵统,都會(huì)把緩存清空「不瘢或者直接使用xcodebuild archive马澈,會(huì)自動(dòng)忽略緩存。每次都要全部重編譯是導(dǎo)致打包速度慢的根本原因弄息。以我們的項(xiàng)目為例痊班,總計(jì) 45min 的打包時(shí)間中,有 40min 都在執(zhí)行xcodebuild這一行命令摹量。
使用 CCache 緩存
最自然的想法就是使用緩存了涤伐,既然蘋(píng)果的緩存不靠譜,那么就找一個(gè)靠譜的緩存缨称,比如 CCache凝果。它是基于編譯器層面的緩存,根據(jù)目前反饋的情況看睦尽,并不存在緩存不一致的問(wèn)題器净。根據(jù)筆者的實(shí)驗(yàn),使用 CCache 確實(shí)能夠較大幅度的提升打包速度当凡,刪除緩存并使用 CCache 重編譯后山害,耗時(shí)只有十幾分鐘。
然而宁玫,CCache 最致命的問(wèn)題是不支持 PCH 文件和 Clang modules粗恢。PCH 的本意是優(yōu)化編譯時(shí)間,我們假設(shè)有一個(gè)頭文件 A 依賴(lài)了 M 個(gè)頭文件欧瘪,其中每個(gè)被依賴(lài)的頭文件又依賴(lài)了 N 個(gè) 頭文件,如下圖所示:
由于#import的本質(zhì)就是把被依賴(lài)頭文件的內(nèi)容拷貝到自己的頭文件中來(lái)匙赞,因此頭文件 A 中實(shí)際上包含了 M * N 個(gè)頭文件的內(nèi)容佛掖,也就需要 M * N? 次文件 IO 和相關(guān)處理妖碉。當(dāng)項(xiàng)目中每增加一個(gè)依賴(lài)頭文件 A 的文件,就會(huì)重復(fù)一次上述的 M * N? 復(fù)雜度的過(guò)程芥被。
PCH 文件的好處是欧宜,這個(gè)文件中的頭文件只會(huì)被編譯一次并緩存下來(lái),然后添加到項(xiàng)目中所有的頭文件中去拴魄。上述問(wèn)題倒是解決了冗茸,但很智障的一點(diǎn)是,所有文件都會(huì)隱式的依賴(lài)所有 PCH 中的文件匹中,而真正需要被全局依賴(lài)的文件其實(shí)非常少夏漱。因此實(shí)際開(kāi)發(fā)中,更多的人會(huì)把 PCH 當(dāng)成一種快速import的手段顶捷,而非編譯性能的優(yōu)化挂绰。前文解釋過(guò),PCH 文件一旦發(fā)生修改服赎,會(huì)導(dǎo)致徹徹底底葵蒂,完完整整的項(xiàng)目重編譯,從而降低編譯速度重虑。正是因?yàn)?PCH 的副作用甚至抵消了它帶來(lái)的優(yōu)化践付,蘋(píng)果已經(jīng)默認(rèn)不使用 PCH 文件了。
用來(lái)取代 PCH 的就是 Clang modules 技術(shù)缺厉,對(duì)于開(kāi)啟了這一選項(xiàng)的項(xiàng)目永高,我們可以用@import來(lái)替代過(guò)去的#import,比如:
@import UIKit;
等價(jià)于
#import
拋開(kāi)自動(dòng)鏈接 framework 這些小特性不談芽死,Clang modules 可以理解為模塊化的 PCH乏梁,它具備了 PCH 可以緩存頭文件的優(yōu)點(diǎn),同時(shí)提供了更細(xì)粒度的引用关贵。
說(shuō)回到 CCache遇骑,由于它不支持 PCH 和 Clang modules,導(dǎo)致無(wú)法在我們的項(xiàng)目中應(yīng)用揖曾。即使可以用落萎,也會(huì)拖累項(xiàng)目的技術(shù)升級(jí),以這種代價(jià)來(lái)?yè)Q取緩存炭剪,只怕是得不償失练链。
distcc
distcc 是一種分布式編譯工具,可以把需要被編譯的文件發(fā)送到其他機(jī)器上編譯奴拦,然后接收編譯產(chǎn)物媒鼓。然而,經(jīng)過(guò)貼吧、貝聊绿鸣、手Q 等應(yīng)用的多方實(shí)驗(yàn)疚沐,發(fā)現(xiàn)并不適合 iOS 應(yīng)用。它的原理是多個(gè)客戶(hù)端共同編譯潮模,但是絕大多數(shù)文件其實(shí)編譯時(shí)間非常短亮蛔,并不值得通過(guò)網(wǎng)絡(luò)來(lái)回傳送,這種方案應(yīng)該只適合單個(gè)文件體量非常大的項(xiàng)目擎厢。在我們的項(xiàng)目中究流,使用distcc大幅度增加了打包時(shí)間,大約耗時(shí) 1 小時(shí)左右动遭。
定位瓶頸
在尋求外部工具無(wú)果后芬探,筆者開(kāi)始嘗試著對(duì)編譯時(shí)間直接做優(yōu)化。為了搞清楚這 40min 究竟是如何花費(fèi)的沽损,我首先對(duì)xcodebuild的輸出結(jié)果進(jìn)行詳細(xì)分析灯节。
使用過(guò)xcodebuild命令的人都會(huì)知道,它的輸出結(jié)果對(duì)開(kāi)發(fā)者并不友好绵估,幾乎沒(méi)有可讀性炎疆,好在還有xcpretty這個(gè)工具可以格式化它:
gem install xcpretty
通過(guò)gem安裝后,只要把xcodebuild的輸出結(jié)果通過(guò)管道傳給xcpretty即可:
xcodebuild -scheme Release ... | xcpretty
下面是官方文檔中的 Demo:
我只對(duì)其中的編譯部分感興趣国裳,所以簡(jiǎn)單的做下過(guò)濾形入,我們就可以得到格式高度統(tǒng)一的輸出:
Compiling A.m
Compiling B.m
Compiling ...
Compiling N.m
到了這一步,終于可以做最關(guān)鍵的計(jì)算了缝左,我們可以通過(guò)設(shè)置定時(shí)器亿遂,計(jì)算相鄰兩行輸出之間的間隔,這個(gè)間隔就是文件的編譯時(shí)間渺杉。當(dāng)然蛇数,也有類(lèi)似的輔助工具做好了這個(gè)邏輯:
npm install gnomon
簡(jiǎn)單的做一下排序,就可以看到最耗時(shí)的前 200 個(gè)文件了是越,還可以針對(duì)文件后綴作區(qū)分耳舅,計(jì)算總耗時(shí)等等。經(jīng)過(guò)排查倚评,我們發(fā)現(xiàn)一半的編譯時(shí)間都花在了編譯 protobuf 文件上浦徊。
工程設(shè)置
除了針對(duì)超長(zhǎng)耗時(shí)的文件進(jìn)行 case-by-case 的分析外,另一種方案是調(diào)整工程設(shè)置天梧。一般來(lái)說(shuō)盔性,我們的持續(xù)集成工具主要是用來(lái)給產(chǎn)品經(jīng)理或者測(cè)試人員使用,用來(lái)體驗(yàn)功能或者驗(yàn)證 Bug呢岗,除非是需要上架 App Store冕香,否則并不需要關(guān)心運(yùn)行時(shí)性能蛹尝。然而在手機(jī)上使用的 Release 模式,默認(rèn)會(huì)開(kāi)啟各種優(yōu)化暂筝,這些優(yōu)化都是犧牲編譯性能箩言,換取運(yùn)行時(shí)速度硬贯,對(duì)于上架的包而言無(wú)可厚非焕襟,但對(duì)于那些 Daily Build 包來(lái)說(shuō),就顯得得不償失了饭豹。
因此鸵赖,加速打包的思路和優(yōu)化的思路是完全互逆的,我們要做的就是關(guān)閉一切可能的優(yōu)化拄衰。這里推薦一篇文章:關(guān)于Xcode編譯性能優(yōu)化的研究工作總結(jié)它褪,可以說(shuō)相當(dāng)全面了。
經(jīng)過(guò)對(duì)其中各個(gè)參數(shù)的查找資料和嘗試關(guān)閉翘悉,按照提升速度的降序排列茫打,簡(jiǎn)單整理幾個(gè):
僅支持 armv7 指令集。手機(jī)上的指令集都屬于 ARM 系列妖混,從老到新依次是 armv7老赤、armv7s 和 arm64。新的指令集可以兼容舊的機(jī)型制市,但舊的機(jī)型不能兼容新的指令集抬旺。默認(rèn)情況下我們打出來(lái)的包會(huì)有 armv7 和 arm64 兩種指令集, 前者負(fù)責(zé)兜底祥楣,而對(duì)于支持 arm64 指令集的機(jī)型來(lái)說(shuō)开财,使用最新的指令集可以獲得更好的性能。當(dāng)然代價(jià)就是生成兩種指令集花費(fèi)了更多時(shí)間误褪。所以在急速打包模式下责鳍,我們只生成 armv7 這種最老的指令集,犧牲了運(yùn)行時(shí)性能換取編譯速度兽间。
關(guān)閉編譯優(yōu)化历葛。優(yōu)化的基本原理是犧牲編譯時(shí)性能,追求運(yùn)行時(shí)性能渡八。常見(jiàn)的優(yōu)化有編譯時(shí)刪除無(wú)用代碼啃洋,保留調(diào)試信息,函數(shù)內(nèi)聯(lián)等等屎鳍。因此提升打包速度的秘訣就是反其道而行之宏娄,犧牲運(yùn)行時(shí)性能來(lái)?yè)Q取編譯時(shí)性能。筆者做的兩個(gè)最主要的優(yōu)化是把Optimize level改成 O0逮壁,表示不做任何優(yōu)化孵坚。
使用虛擬磁盤(pán)。編譯過(guò)程中需要大量的磁盤(pán) IO,這主要發(fā)生在Derived Data目錄下卖宠,因此如果內(nèi)存足夠巍杈,可以考慮劃出 4G 左右的內(nèi)存,建一個(gè)虛擬磁盤(pán)扛伍,這樣將會(huì)把磁盤(pán) IO 優(yōu)化為 內(nèi)存 IO筷畦,從而提高速度。由于打包機(jī)器每次都會(huì)重編譯刺洒,因此并不需要擔(dān)心重啟機(jī)器后緩存丟失的問(wèn)題鳖宾。
不生成 dYSM 文件,前文已經(jīng)介紹過(guò)逆航。
一些其他的選項(xiàng)鼎文,參考前面推薦的文章。
在以上幾個(gè)操作中因俐,精簡(jiǎn)指令集的作用最大拇惋,大約可以把編譯時(shí)間從 45 min 減少到 30min 以?xún)?nèi),配合關(guān)閉編譯優(yōu)化抹剩,可以進(jìn)一步把打包時(shí)間減少到 20min撑帖。虛擬磁盤(pán)大約可以減少兩三分鐘的編譯時(shí)間,dSYM 耗時(shí)大約二十秒吧兔,其它選項(xiàng)的優(yōu)化程度更低磷仰,大約在幾秒左右,沒(méi)有精確測(cè)算境蔼。
因此灶平,一般來(lái)說(shuō)只要精簡(jiǎn)指令集并關(guān)閉優(yōu)化即可,有條件的機(jī)器可以使用虛擬磁盤(pán)箍土,不建議再做其它修改逢享。
二進(jìn)制化
二進(jìn)制化主要指的是利靜態(tài)庫(kù)代替源碼,避免編譯吴藻。前文已經(jīng)介紹過(guò)如何分析文件的耗時(shí)瞒爬,因此二進(jìn)制化的收益非常容易計(jì)算出來(lái)。由于團(tuán)隊(duì)分工問(wèn)題沟堡,筆者沒(méi)有什么二進(jìn)制化的經(jīng)驗(yàn)侧但,一般來(lái)說(shuō)這個(gè)優(yōu)化比較適合基礎(chǔ)架構(gòu)組去實(shí)施。
硬件加速
以上主要是通過(guò)修改軟件的方式來(lái)加速打包航罗,自從公司申請(qǐng)了 2013 年款 Mac Pro(Xeon-E5 1630 6 核 12 線程禀横,16G 內(nèi)存,256G SSD 標(biāo)配粥血,下文簡(jiǎn)稱(chēng) Mac Pro)后柏锄,不需要修改任何配置酿箭,僅僅是簡(jiǎn)單的遷移打包機(jī)器,就可以把打包時(shí)間降低到 15 min趾娃,配和上一節(jié)中的前三條優(yōu)化缭嫡,最終的打包時(shí)間大概在 10min 以?xún)?nèi)。
在我的黑蘋(píng)果(i7 7820x 8 核 16 線程抬闷,16G 內(nèi)存妇蛀,三星? PM 961 512G SSD,下文簡(jiǎn)稱(chēng)黑蘋(píng)果)上饶氏,即使不開(kāi)啟任何優(yōu)化讥耗,從零開(kāi)始編譯也僅需 5min。如果將 protobuf 文件二進(jìn)制化疹启,再配合一些工程設(shè)置的優(yōu)化,我不敢想象需要花多長(zhǎng)時(shí)間蔼卡,預(yù)計(jì)在 4min 左右吧喊崖,速度提升了大概 11 倍。
編譯是一個(gè)考驗(yàn)多核性能的操作雇逞,在我的黑蘋(píng)果上荤懂,編譯時(shí)可以看到 8 個(gè) CPU 的負(fù)載都達(dá)到了 100%,因此在一定范圍內(nèi)(比如 10 核以?xún)?nèi))塘砸,提升 CPU 核數(shù)遠(yuǎn)比提升單核主頻對(duì)編譯速度的影響大节仿。至于某些 20 核以上、單核性能較低的 CPU 編譯性能如何掉蔬,希望有經(jīng)驗(yàn)的讀者給予反饋廊宪。
優(yōu)化點(diǎn)總結(jié)
下表總結(jié)了文章中提到的各種優(yōu)化手段帶來(lái)的速度提升,參考原始時(shí)間均為 45 min(打包機(jī)器:13 寸? MacBook Pro):
方案序號(hào)優(yōu)化方案優(yōu)化后耗時(shí) (min)時(shí)間減少百分比
1不常修改的文件二進(jìn)制化2544.4%
2精簡(jiǎn)指令集2740%
3關(guān)閉編譯優(yōu)化3815.6%
4使用 Mac Pro1566.7%
5虛擬磁盤(pán)426.7%
6公司現(xiàn)行方案(2+3+4+5)980%
7黑蘋(píng)果588.9%
8終極方案(1+2+3+5+7)4(預(yù)計(jì))91.1%(預(yù)計(jì))
嚴(yán)格意義上講女轿,文章有點(diǎn)標(biāo)題黨了箭启,因?yàn)橐痪湓拋?lái)說(shuō)就是:
能用硬件解決的問(wèn)題,就不要用軟件解決蛉迹。