<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          iOS Pod 構建緩存方案

          共 6100字,需瀏覽 13分鐘

           ·

          2021-07-07 17:30

          本文由作者 猶落 授權轉載

          猶落,公眾號:零行代碼iOS Pod 構建緩存方案

          前言

          上周 SwiftGG 在北京舉辦了一場技術沙龍[1],其中字節(jié)跳動的《動態(tài)化研發(fā)模式-ARK》和滴滴的《使用 Xcode Cache 為構建打包提速》,都表達了對研發(fā)效率的探索和分享。

          我當然沒有參加,只是今天無意中拿到了相關文檔,其中一份PPT使用 Xcode Cache 為構建打包提速》(下文簡稱PPT),初衷與我兩年前的方案一致,都是使用源碼編譯緩存手段,避免Pod二進制可能的運行時問題,同時加速構建。

          PPT的細節(jié)不盡詳述,我的理解是通過各種手段,將Xcode的編譯緩存目錄復用,使得構建打包的增量編譯效果與本機開發(fā)的一致。

          而我的這個Pod緩存方案,基于MD5自己實現(xiàn)了一套緩存策略,達到增量編譯的效果。

          那就借此機會,重新整理一下吧。圖為我兩年前發(fā)布在公司內(nèi)網(wǎng)的文章《iOS工程自動化緩存實現(xiàn)極速構建》。

          目標

          最初的目標,是減少CI構建機上的 iOS App 構建時間,以提高測試階段多次提交多次交付的效率。至于開發(fā)者機器上的編譯耗時,暫時不在這里的討論范圍。

          iOS App 是典型的 CocoaPods 工程,包括 GitHub 庫、公司私有庫以及本地組件化的 Development Pods

          為什么不使用Pod二進制

          最重要的原因是人力成本,包括上百個私有庫的獨立倉庫、獨立構建、版本號維護和集成,這些都比較繁瑣,而且難以完全自動化(盡管可以部分自動化)。

          另一個不能忽視的原因在于編譯依賴與運行依賴的不一致性,導致的 Pod 二進制運行問題。

          我以前舉了個編譯宏的例子,假如AAA的宏發(fā)生變化,但BBB沒有重新編譯,這時實際運行結果就會不符合預期。

          PPT 中舉的例子也很直觀。B的方法發(fā)生變化,但A二進制沒有重新編譯,雖然編譯鏈接通過,但運行起來 PodA 就會因為unrecognized selector發(fā)生崩潰。

          截圖來源PPT

          這個問題,在組件二進制推進比較完善的美團,在文章中《美團外賣iOS多端復用的推動、支撐與思考[2]》中也有提及

          這里有一個問題需要解決,即引用二進制帶來的弊端,顯而易見的就是將編譯期的問題帶到了運行期。某個宏修改了,但是編譯完的二進制代碼不感知這種改動,并且依賴版本不匹配的話,原本的方法缺失編譯錯誤,就會帶到運行期發(fā)生崩潰。解決此類問題的方法也很簡單,就是在所有的打包工程中都配置了打包自動切換源碼。二進制僅僅用來在開發(fā)中獲得更高的效率,一旦打提測包或者發(fā)布包都會使用全源碼重新編譯一遍。

          但不可否認,大型App的全源碼重新編譯耗時實在嚴重影響研發(fā)效率。

          思路

          最快的編譯是不編譯。

          盡管迭代了多個版本,但思路沒變。在編譯前后增加腳本,由全量編譯改為增量編譯。

          每次構建在pod update之后、開始編譯之前,根據(jù)編譯組件產(chǎn)物所需的源文件、編譯參數(shù)、依賴文件信息,生成一個MD5,根據(jù)這個MD5查詢緩存產(chǎn)物,以決定是復用緩存還是重編譯。這個思路來源于 Xcode的編譯過程,同時也是受開源的ccache啟發(fā),但ccache的并發(fā)性能、兼容性、穩(wěn)定性在實測中并沒有達到預期。

          方案

          目前的方案已經(jīng)在幾個app項目穩(wěn)定構建運行一段時間了,包括測試包和AppStore正式包。

          特性如下

          1. 支持 Objective-C
          2. 支持 Swift
          3. 支持 CocoaPodsgenerate_multiple_pod_projects 以及 incremental_installation
          4. 支持 com.apple.product-type.library.staticcom.apple.product-type.bundletarget類型
          5. 支持不同工作目錄、不同工程、不同分支下的組件緩存復用
          6. 支持不同的 configuration
          7. 支持Pods工程中的PodsDevelopment Pods
          8. 不支持主工程或其他工程的文件緩存
          9. 使用腳本集成,對項目倉庫無侵入

          為什么不使用文件時間戳

          Xcode使用的緩存策略之一,就是文件時間戳變化會重新編譯。這經(jīng)常導致一些不必要的重新編譯,尤其是在pod update之后。所以PPT的方案采取了修正時間戳的手段。

          截圖來源PPT

          而實際上,我們的構建機器很可能多個項目、多個分支并發(fā)構建,這會導致不同的工作目錄從而導致完全重新編譯,所以修正時間戳的作用比較受限。

          我當時首先進行了全工程所有文件的MD5計算,腳本運行耗時也只是幾十秒。如果每次構建可以穩(wěn)定減少幾分鐘甚至幾十分鐘,那么這幾十秒的開銷也是值得的。當然這個開銷也是我這個方案不適合在本機開發(fā)使用的主要原因之一。

          實踐證明,使用MD5的方案,可以使得Pod構建緩存可以在不同的App、不同的分支、不同的工作目錄中盡可能復用,加速效果與Pod二進制一致,編譯效果與源碼編譯一致,同時達到既安全又快速的效果。

          如何獲取編譯參數(shù)

          編譯參數(shù)主要是指Xcode傳遞給編譯器的參數(shù)以及鏈接器的參數(shù)。由以下幾個來源合并生成

          • Configuration
          • Xcode Project xcconfig File
          • Xcode Project Build Settings
          • Target xcconfig File
          • Target Build Settings
          • File Compiler Flags

          將以上內(nèi)容加入到PodMD5的計算輸入中,使得編譯參數(shù)不同就會重新編譯。比如很多工程會在Podfilepost_install里注入一些編譯宏,不同的宏應該需要重新編譯。注意這里的編譯參數(shù)是批量讀取而不是逐個獲取的,理論上不存在Xcode升級引起的不兼容的問題。

          另外,考慮到實際上的SEARCH_PATHS不參與實際編譯(參與實際編譯的是依賴的頭文件),所以也會去除相關的SEARCH_PATHS以減少不必要的緩存miss。舉例如下

          • FRAMEWORK_SEARCH_PATHS
          • HEADER_SEARCH_PATHS
          • LD_RUNPATH_SEARCH_PATHS
          • LIBRARY_SEARCH_PATHS
          • USER_HEADER_SEARCH_PATHS

          如何分析依賴

          兩年前第一版的方案使用的是手動正則遞歸解析#include#import進行頭文件的依賴分析,運行了幾個月,后來發(fā)現(xiàn)部分個例場景下有Bug導致了匹配復用到錯誤的緩存,雖然當時修復了,但始終不靠譜。

          就正如PPT中提到了的這個問題

          截圖來源PPT

          那有什么方法可以100%保證分析結果的準確呢?有的,調(diào)用編譯器進行預編譯,獲取所有的依賴文件。但這個開銷太大,以至于總體結果很有可能是負優(yōu)化。

          還有其他靠譜的分析依賴的方法呢?還有的,Xcode使用clangswift編譯時,默認都會生成.d的依賴分析結果在中間產(chǎn)物目錄,里面包含某個文件編譯時所需的所有頭文件。

          對應的編譯命令精簡一下表達如下

          .../clang ... -MMD -MT dependencies -MF .../YYWebImage.build/Objects-normal/arm64/YYWebImageManager.d ....../swift-frontend ... -emit-dependencies-path .../SwiftMessages.build/Objects-normal/arm64/SwiftMessages.d ...

          舉例看看YYWebImageManager.d。swiftclang生成的.d會復雜一些,不過問題不大。

          dependencies: \  /Users/dengweijun/xxx/Pods/YYWebImage/YYWebImage/YYWebImageManager.m \  /Users/dengweijun/xxx/Pods/Target\ Support\ Files/YYWebImage/YYWebImage-prefix.pch \  /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.5.sdk/usr/include/mach-o/compact_unwind_encoding.modulemap \  /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.5.sdk/usr/include/mach-o/dyld.modulemap \  /Users/dengweijun/xxx/Pods/YYWebImage/YYWebImage/YYWebImageManager.h \  /Users/dengweijun/xxx/Pods/YYWebImage/YYWebImage/YYWebImage.h \  /Users/dengweijun/xxx/Pods/YYWebImage/YYWebImage/YYImageCache.h \  /Users/dengweijun/xxx/Pods/YYWebImage/YYWebImage/YYWebImageOperation.h \  /Users/dengweijun/xxx/Pods/Headers/Private/YYImage/YYImage.h \  /Users/dengweijun/xxx/Pods/Headers/Private/YYImage/YYFrameImage.h \  /Users/dengweijun/xxx/Pods/Headers/Private/YYImage/YYAnimatedImageView.h \  /Users/dengweijun/xxx/Pods/Headers/Private/YYImage/YYSpriteSheetImage.h \  /Users/dengweijun/xxx/Pods/Headers/Private/YYImage/YYImageCoder.h

          好家伙,簡直完美。

          那問題來了,在編譯之前的時候,需要這個依賴信息生成MD5作為查詢組件緩存的key,這時怎樣在不編譯的情況下高效拿到這個依賴信息呢?

          我的做法是使用源文件+編譯參數(shù)生成一級MD5,然后查詢一級MD5對應的依賴信息列表,遍歷這個列表,再遍歷依賴信息里的所有文件。如果找到一份依賴信息,與當前工作目錄的對應文件都完全匹配(MD5一致),則認為依賴一致,使用源文件+編譯參數(shù)+依賴信息生成最終MD5,作為查詢組件緩存的key。另外,為了達到最佳的緩存命中效果,會將緩存的查詢key中的絕對路徑改為相對路徑。

          這種多依賴多緩存的方案,盡可能保存并關聯(lián)每次的編譯結果,無論是對比Xcode只有一份依賴一份緩存的方案,還是對比PPT中整個工作目錄單依賴多緩存的方案,都使得增量編譯更容易命中緩存。實測遍歷依賴文件和MD5計算的開銷在可接受的預期范圍內(nèi)。

          怎樣復用緩存

          每次編譯成功之后,將上述的組件粒度的MD5作為緩存的key,將依賴信息(.d以及所有依賴文件的MD5)和組件產(chǎn)物(.a.bundle等)復制到構建機器的指定的全局緩存目錄。由于復制到同一臺機器不需要依賴網(wǎng)絡,所以整個緩存方案更加穩(wěn)定可靠。在這個緩存目錄下使用LRU的策略清除長期不會命中的緩存,在空間和時間上取得平衡。

          而每次編譯之前,查詢組件緩存,對于命中緩存的組件(target

          1. 刪除工程文件的target,使得Xcode不編譯這個target的相關文件
          2. 將緩存產(chǎn)物從指定的全局緩存目錄復制到這個targetXcode產(chǎn)物目錄

          這種對工程文件的破壞性修改,只能在構建機自動完成,也是不適合在本機開發(fā)使用的第二個主要原因。

          由于命中緩存后不編譯的組件的頭文件路徑和產(chǎn)物鏈接路徑保持不變,所以理論上不影響其他組件以及主工程的編譯。

          整個腳本有些實際操作上的細節(jié)處理,比如

          1. 刪除target之后若其dependency需要重新編譯,需要保證能夠觸發(fā)其編譯。
          2. 由于xcodebuild archive本身會刪除緩存,所以需要往Pods工程注入腳本使得在xcodebuild archive開始時才執(zhí)行實際復制

          怎樣使用

          構建腳本修改示意如下,增加兩行ruby腳本即可完成接入。即將開源,敬請期待。

          pod updateruby hy_auto_build_cache_v4.rb -stage apply -configuration Release # 查詢和復用緩存xcodebuild archive xxxruby hy_auto_build_cache_v4.rb -stage cache -configuration Release # 新增緩存

          為什么以組件為粒度緩存

          開源的ccache是以文件(如目標文件.o)為粒度緩存,我自己也寫過類似的方案,修改CC以使用自己的編譯器來轉發(fā)編譯,但實測上以文件為粒度的緩存方案,雖然有更精確的編譯參數(shù)控制和依賴文件分析,以及有更高的命中率,但在平均情況下總體性能明顯不如以組件為粒度緩存的方案。我的理解是主要兩個原因,以文件為粒度的緩存方案,一個是Xcode的實際編譯的計算開銷依然非常大,另一個是逐個文件加入緩存的緩存計算開銷也不少。所以最后在構建機使用的是以組件為粒度的緩存方案,直接整個組件移除編譯,直接減少Xcodebuild tasks總數(shù)。

          總結

          實測在Apple M1的機器上,使用這個Pod緩存方案,在足夠組件化的工程中,完全命中緩存的情況下,xcodebuild編譯耗時從9分鐘下降至1分鐘(不包括另外1分鐘左右的緩存開銷),效果顯著。平時的實際編譯耗時取決于增量修改的影響范圍,如果修改了較底層的頭文件,可能會觸發(fā)較大范圍的重新編譯。

          這個Pod緩存方案雖然受限于Pods工程,但近乎完美的安全的緩存查詢策略,顯著的命中提速效果,較低的開銷,都證明了這個方案的實用性。

          歡迎交流。

          參考資料

          [1]

          技術沙龍: https://www.bagevent.com/event/7454056

          [2]

          美團外賣iOS多端復用的推動、支撐與思考: https://tech.meituan.com/2018/06/29/ios-multiterminal-reuse.html


          瀏覽 117
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产青青操 | 久久久久久久久久免费看视频 | 国精产品一区一区三区有限是什么 | 亚洲av观看 | 黄色考逼视频免费观看网站www |