攜程旅行App iOS工程編譯優化實踐

引言

開發效率的提升,是開發者關注的一個永恆的話題。對於iOS而言,編譯速度一直是影響iOS開發和集成測試效率關鍵的一環。

攜程旅行App iOS工程編譯,經歷了從全源碼編譯到工程組件化,細分Bundle,再到細分Bundle基礎上的進一步優化四個階段。每次的優化改造都是不斷結合業務反饋,深入瞭解xcode編譯過程後的成果。

一、背景

簡單回顧一下在做Bundle拆分之前的情況,當時整個iOS工程的所有代碼都在一起,並未做工程拆分和解耦,編譯時全都是源碼編譯,數百萬行代碼全部編譯完成要將近一個小時。所有的開發人員都在一個工程裏開發,如果因爲某個人提交的代碼有問題(這是常常會發生的),導致編譯了很長時間之後才報錯,更是耽誤時間,嚴重影響開發效率。對於測試人員來說,每次需要驗證一個功能時打包測試都需要至少等待幾十分鐘,這是極大的資源浪費。

這個時候的Build過程是全源碼complie,幾千上萬個文件都需要編譯、鏈接,效率可想而知。

所以爲了提高開發和測試的效率,提高iOS工程的編譯速度刻不容緩。

二、優化方案

2.1 工程組件化

第一個優化是把整個工程的編譯過程打散,把代碼按照業務線拆分成一個個獨立的子工程,每個子工程的編譯過程都是獨立的。每個子工程只需要保證自己工程的源碼能夠編譯成功,對外輸出統一的靜態庫和資源文件包的產物。這個產物我們叫做Bundle。

單個業務工程(Bundle):

App Build:

對於單個業務來說,編譯時間大大縮短,整個Build過程變成單工程complie,多工程link,極大減少了Build過程中的complie花費的時間。

這樣有兩個好處:

1)對於開發人員,每個業務開發只需要把自己這個子工程切爲源碼引用,把其他非自己模塊的子工程全部用靜態庫依賴,本地編譯也只需要編譯自己的子工程,可以大大提升本地開發編譯速度。

2)對於測試人員,打包過程就變成了把所有已經編譯好的子Bundle靜態庫鏈接到一個殼工程裏,不需要對每個文件進行編譯,可以很快的打包測試驗證。

2.2 增量編譯

在工程組件化之後,在持續集成平臺上單個Bundle的打包時間還是過長。因此框架團隊開始研究單個Bundle在持續集成平臺上增量編譯的可能性。

經過調研,最終選定CCache做爲解決方案。CCache是一個編譯工具,可以將Xcode編譯文件緩存起來,從而達到編譯提速。

針對本地開發該方案具有優勢,但是在結合自研的移動發佈平臺MCD(Mobile Continuous Delivery)(後面簡稱【發佈平臺】)上使用時效果並沒有達到預期,主要有兩點原因:

1)同一Bundle多分支共存 :App會存在大小版本同時開發的情況,在發佈平臺中也就會存在不同版本、不同分支的情況。

2)緩存管理不便 :發佈平臺打包機器通常僅有250G磁盤空間,當面臨磁盤壓力時,需要靈活的清理策略。

最終框架團隊採用了自管理,能做到緩存物理隔離,同時也就省去了環境配置的步驟。

增量編譯具體實現:

1)合併有變動的文件

  • 打包任務會根據新的 commitId 下載一份代碼副本,不能直接使用該副本,因爲代碼文件內容沒有變動,僅僅是文件屬性的變動也會導致 xcodebuild 緩存不生效。因此需要副本和工作區內的源碼做diff,僅僅合併內容有變動的文件。
  • 使用 python 的 filecmp 實現合併代碼邏輯,並且支持配置 ignore。
  • xcodebuild 指定 -derivedDataPath 設置緩存路徑,並將該目錄配置到 diff ignore中。

2)提供清除緩存的功能

  • xcodebuild的緩存有時候會出問題,比如修改了c++文件後有時並不會生效,這種需要提供清除緩存的功能,可以由開發自由選擇使用。

截止到以上兩步,Native已經基本實現了增量編譯,但是實際使用還不夠。因爲打包主要是在集成系統平臺上面完成的,集成平臺打包有多臺機器。

攜程旅行App的打包Jenkins採用的是master-slave模式,一個Job下會有多個節點,Job是隨機抽取的節點。爲了提高增量編譯的命中率,必須要讓Bundle和節點關聯起來。比如:有ABCD四個節點,HotelBundle每次都落到A節點,這樣才能保證A節點中HotelBundle的xcodebuild緩存有效,並且代碼diff差異最小。

具體實現:

1)保留Jenkins Job的工作區

該步驟是在Jenkins Job的配置中操作,取消勾選下圖中的Delete workspace before build starts

2)使用Jenkins插件建立Bundle和節點的關聯

基於Jenkins Label Parameter Plugin,並做改造,實現僞隨機,以保證關聯的節點下線之後,能使用候補節點正常工作。

發佈平臺前端提供關聯配置,業務可以按需選擇使用。

通過以上步驟就實現了增量編譯,但是該方案針對swift不生效。swift在Release模式採用的全量編譯(如下圖),做整體優化。不過swift Bundle可以採用上述Bundle拆分的方案。

以某一個編譯源碼文件197個、資源文件142個的Bundle爲例看下效果。

採用增量編譯後,Bundle編譯耗時由116s降爲9s。

2.3 Bundle細分

最初攜程旅行App的Bundle都是按照業務來拆分的,比如:酒店就一個Hotel Bundle,在當時編譯速度已經不慢了。但是隨着業務的發展,單個Bundle中業務代碼越來越多,文件越來越多,導致編譯又會變慢。這時,可以將單個Bundle按照功能做更細粒度的拆分,比如酒店拆分出了酒店主工程、酒店基礎工程。

更細粒度的Bundle拆分還能帶來以下其他收益:

  • 加快本地開發編譯:某個功能的開發人員只需要將自己這個功能模塊切爲源碼,其他模塊全用靜態庫,提高本地開發編譯效率。
  • 爲其他獨立app提供更細粒度的模塊功能支持:我廠的很多獨立App都是共用一套框架和基礎組件的,按功能模塊細粒度的拆分出獨立的模塊Bundle後,可以使獨立app在選擇基礎組件時按需選擇。

2.4 合理設置頭文件搜索路徑

業務工程往往會大量依賴基礎庫代碼,在本工程編譯過程中,也需要查找到引用的基礎代碼的頭文件。

因爲代碼還是在同一個倉庫裏,之前的方案是頭文件搜索設置還是指向本地的基礎框架代碼,使用循環搜索的方式。

這樣的好處是任何一個頭文件的修改,使用方可以馬上感知到。

缺點就是頭文件沒有特意爲方便調用進行組織,搜索起來特別費時。

經過統計,Hotel一個文件的編譯往往都是秒級別。一整個工程編譯下來就是十幾分鍾。

因此框架團隊意識到必須要和第三方庫一樣,在目前的.a和資源文件之外,提交include目錄包含所有會被外部使用的頭文件。

同時,考慮到iOS開發向Swift轉型的需要,如果在include目錄的基礎上,還能夠提供一份基於include裏頭文件的module.mapmodule文件。將方便後期業務方向Swift的遷移。

具體方法是:

1)首先框架的Bundle,在工程設置中點擊工程的Target→Build Phases→Copy Files點擊+,輸入.h把需要暴露的頭文件都添加上。

這樣會在輸出產物的Build目錄下,多一個include目錄,再通過腳本去把這個目錄裏面的所有文件複製出來,同時生成module.mapmodule。

2)使用的時候,將頭文件搜索路徑設置到include目錄,並且設置爲非遞歸搜索。

驗證下來,Hotel工程修改之後的Build時間爲7分鐘,相比修改之前的19分鐘,時間減少了63%。

2.5 建立中央緩存

費雷德里克·布魯克斯說軟件工程領域沒有銀彈。通過以上優化後,減少了編譯時間,提升了開發和集成測試的效率,但這也不是解決編譯速度問題的銀彈。隨着業務的不斷使用,又出現了新的問題:Bundle拉取時間過長。

Bundle化方案各個業務的靜態庫生成都是在發佈平臺上編譯的,業務在本地開發的時候再使用框架的腳本拉取bundle到本地。發佈平臺上打測試包的時候也是需要拉取所有Bundle。

發佈平臺打包過程如下:

1)初始化Jenkins工作區,下載代碼副本

2)下載Bundle

3)使用xcodebuild生成ipa

4)上傳ipa和符號表

5)Job狀態回調

整個過程共耗時7分鐘,目前攜程旅行App iOS最新的版本的上線Bundle將近70個,每個Bundle的靜態庫支持arm64、x84_64等指令集,所有Bundle加起來有4G大小,即使在內網全量下載耗時也要2~3分鐘。

比如酒店某一Bundle:

所有Bundle全量更新一次耗時:

針對這個問題,解決方案是建立中央緩存。

在用戶根目錄下,建立一個隱藏的目錄.iOSBundleRepo,按照Bundle的版本號存儲,同一Bundle可存在多個版本。工具下載Bundle時優先判斷緩存,未命中時纔開始下載並且緩存到repo中。

建立中央緩存還能帶來其他好處:在發佈平臺做預緩存,使用定時任務更新中央緩存,進一步節省下載耗時。

該方案實際上採用的是空間換時間的策略,隨着時間推移,將會帶來磁盤不足的問題,所以必須要實現清理機制。

針對不同使用場景需要採用不同的緩存清理策略,具體如下:

  • 本地開發:該模式下,開發可以自由選擇更新最新Bundle和僅更新配置,緩存使用不頻繁。所以將同一Bundle版本個數調低,緩存有效期拉長。
  • 持續集成:發佈平臺打包較爲頻繁,緩存使用比較頻繁,並且Bundle版本變動較快,所以將同一Bundle個數調高,緩存過期時間設置爲一天。

最終,打包耗時由原來的7分鐘降爲5分鐘。

三、存在的問題和思考

軟件開發工程沒有銀彈,大家都是在焦油坑裏掙扎。

Bundle的方案節省了編譯的時間,提高了開發的效率,方便了持續集成和測試。

爲了提高單Bundle編譯速度而導出頭文件的方案,犧牲了一定的靈活性換來了編譯速度的提高。頭文件沒有了代碼中的直接搜索,框架開發人員從共同開發者真正變成了庫提供者,這就要求每一次都接口的修改都要及時更新並導出。

任何一個技術方案肯定是在權衡各方面之後做出取捨的結果。框架團隊爲了提高iOS Build速度,通過自研的方案,做了拆分Bundle,優化頭文件搜索路徑,增量編譯,建立中央緩存等步驟,基本上滿足了現有我廠各業務線的日常開發需求。

作者介紹

天超,攜程資深軟件工程師,關注iOS研發,喜歡用腳本語言解決各種難題。

本文轉載自公衆號攜程技術(ID:ctriptech)。

原文鏈接

攜程旅行App iOS工程編譯優化實踐

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章