抖音研發效能建設 - CocoaPods 優化實踐

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"背景"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"抖音很早就接入 CocoaPods 進行依賴管理了,項目前期抖音只有幾十個組件,業務代碼也基本在殼工程內,CocoaPods 可以滿足業務研發的需求,但是隨着業務的不斷迭代,代碼急劇膨脹,同時抖音工程也在進行架構優化,比如工程組件化改造,組件的數量和複雜度不斷增加:組件(Pod)數量增加到 400+ ,子組件(Subspec)數量增加到 1500+ ,部分複雜組件的描述文件(podspec)膨脹到 1000+ 行,這導致了依賴管理流程(主要是 Pod Install)的效率不斷下降,同時也導致了 Xcode 檢索和構建效率下降。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"除了效率下降外,我們也開始遇到一些 CocoaPods 潛在的穩定性問題,比如在 CI\/CD 任務併發執行的環境下 Pod Install 出現大量失敗,這些問題已經嚴重影響了我們的研發效率。在超大工程、複雜依賴、快速迭代的背景下,CocoaPods 已經不能很好地支撐我們的研發流程了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"反饋最多就是 Pod Install 慢,經常會有同學反饋 Pod Install 流程慢,涉及到決議流程慢,依賴下載慢、Pods 工程生成慢等"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"本地 Source 倉庫沒更新,經常導致找不到 Specification,Pod Install 失敗"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"依賴組件多,循環依賴報錯,但是難以找到循環鏈路"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"依賴組件多,User 工程複雜度,導致 Pod Install 後 Xcode 工程索引慢,卡頓嚴重"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":5,"align":null,"origin":null},"content":[{"type":"text","text":"依賴組件多,工程構建出現不符合預期的失敗問題,比如 Arguments Too Long"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":6,"align":null,"origin":null},"content":[{"type":"text","text":"研發流程上,有部分研發同學本地誤清理了 CocoaPods 緩存,導致工程編譯或者鏈接失敗"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":7,"align":null,"origin":null},"content":[{"type":"text","text":"組件拆分後,新添加文件必須 Pod Install 後纔可以被其他組件訪問,這拖慢了研發效率"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們開始嘗試在 0 侵入、不影響現有研發流程的前提下,改造 CocoaPods 做來解決我們遇到的問題,並且取得了一些收益。在介紹我們的優化前,我們會先對 CocoaPods 做一些介紹, 我們以 CocoaPods 1.7.5 爲例來做說明依賴管理的核心流程「Pod Install」"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"Pod Install"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們以一個 MVP 工程「iOSPlayground」爲例子來說明,iOSPlayground 工程是怎麼組織的:"}]},{"type":"embedcomp","attrs":{"type":"table","data":{"content":"
iOSPlayground.xcodeproj殼工程,包含 App Target:iOSPlayground
iOSPlayground殼工程文件目錄,包含資源、代碼、Info.plist
Podfile聲明 User Target 的依賴
Gemfile聲明 CocoaPods 的版本,這裏是 1.7.5"}}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們在 Podfile 中爲 Target「iOSPlayground」引入 SDWebImage 以及 SDWebImage 的兩個 Coder,並聲明這些組件的版本約束"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"platform :ios, '11.0'\nproject 'iOSPlayground.xcodeproj'\ntarget 'iOSPlayground' do\n  pod 'SDWebImage', '~> 5.6.0'\n  pod 'SDWebImageLottieCoder', '~> 0.1.0'\n  pod 'SDWebImageWebPCoder', '~> 0.6.1'\nend\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後執行 Pod install 命令 "},{"type":"codeinline","content":[{"type":"text","text":"bundle exec pod install"}]},{"type":"text","text":",CocoaPods 開始爲你構建多依賴的開發環境;整個 Pod Install 流程最核心的就是 "},{"type":"codeinline","content":[{"type":"text","text":"::Pod::Installer"}]},{"type":"text","text":" 類,Pod Install 命令會初始化並配置 Installer,然後執行 install! 流程,install! 流程主要包括 6 個環節"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/09\/090c4ec86bd7ba3e1fc9d4f87e5deee8.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"def install!\n  prepare\n  resolve_dependencies # 依賴決議\n  download_dependencies # 依賴下載\n  validate_targets # Pods 校驗\n  generate_pods_project # Pods Project 生成\n  if installation_options.integrate_targets?\n    integrate_user_project # User Project 整合\n  else\n    UI.section 'Skipping User Project Integration'\n  end\n  perform_post_install_actions # 收尾\nend\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面會對這 5 個流程做一些簡單分析,爲了簡單起見,我們會忽略一些細節。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"準備階段"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個流程主要是在 Pod Install 前做一些環境檢查,並且初始化 Pod Install 的執行環境。"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/78\/786a85db421a8212cf846b93d610893a.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"依賴分析"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個流程的主要目標是分析決議出所有依賴的版本,這裏的依賴包括 Podfile 中引入的依賴,以及依賴本身引入的依賴,爲 Downloader 和 Generator 流程做準備。"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/02\/027cc9f00a195fb330722934da446c5c.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個過程的核心是構建 Molinillo 決議的環境:準備好 Specs 倉庫,分析 Podfile 和 Podfile.lock,然後進行 Molinillo 決議,決議過程是基於 DAG(有向無環圖)的,可以參考下圖,按照最優順序依次進行決議直到最後決議出所有節點上依賴的版本和來源。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/f7\/f7972a700bb8f7b126e2d059b8a3beba.gif","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"iOSPlayground 工程最後決議出的依賴列表是:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/20\/209d1ba49b70a7122c010c6ef0d719aa.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"基於最後決議的結果我們就可以獲取 Specifications、生成 Aggregate Targets 和 Pod Targets。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"Aggregate Targets:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/a0\/a04f1485c9fe25ae4c3d34afa6837376.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"Pod Targets:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/7f\/7feb5200211900c251ee6f9e280e6fcd.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"embedcomp","attrs":{"type":"table","data":{"content":"
Version一般是用點分割的可以比較的序列,組件會以版本的形式對外發布
Requirement一個或者多個版本限制的組合
SourceSpecs 倉庫,組件發版的位置,用於管理多個組件多個版本的一組描述文件
DependencyUser Target 的依賴或者依賴的依賴,由依賴名稱、版本、約束、來源構成
PodfileRuby DSL 文件,用於描述 Xcode 工程中 Targets 的依賴列表
Podfile.lockYAML 文件,Pod Install 後生成的依賴決議結果文件
PodspecRuby DSL 文件,用於描述 Pod,包括名稱、版本、子組件、依賴列表等
Pod Target一個組件對應一個 Pod Target
Aggregate Target用來聚合一組 Pod Target,User Target 會依賴對應的 Aggragate Target
$HOME\/.cocoapods\/repos\/本地存儲需要使用的 Specs 倉庫"}}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"依賴下載"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個流程的目標是下載依賴,下載前會根據依賴分析的結果 specifications 和 sandbox_state 生成需要下載的 Pods 列表,然後串行下載所有依賴。這裏只描述 Cache 開啓的情況,具體流程可以參考下圖:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/57\/57820013dc4c61aad57560f76b67ebf9.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"CocoaPods 會根據 Pod 來源選擇合適的下載器,如果是 HTTP 地址,使用 CURL 進行下載;如果是 Git 地址,使用 Git 進行拉取;CocoaPods 也支持 SVN\/HG\/SCP 等方式。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"iOSPlayground 工程的下載流程:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/a9\/a9b6443aed5402bc58f578889511cc36.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"embedcomp","attrs":{"type":"table","data":{"content":"
$HOME\/Library\/Caches\/CocoaPods\/Pod 本地緩存目錄,用存儲下載到本地的 Pod,避免二次下載"}}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Pods 校驗"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個階段主要是檢查 Pod 描述文件 Speification、Pod Targets 和 Aggregate Targets 配置是否正確,從而保證 Pod Install 後可以進行正確構建,一般包括四個流程的檢查。"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/3b\/3bef76c8606a175a016e3ef3f3cedeed.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Pods 工程生成"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個流程的目標是生成 Pods 工程,根據依賴決議的結果 Pod Targets 和 Aggregate Targets,生成 Pods 工程,並生成工程中 Pod Targets 和 Aggregate Targets 對應的 Native Targets。"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/07\/070dec8b842d6f578442a4dbb39e47bc.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"CocoaPods 提供兩種 Project 的生成策略:Single Project Generator 和 Multiple Project Generator,Single Project Generator 是指只生成 Pods\/Pods.xcodeproj,Native Pod Target 屬於 Pods.xcodeproj;Multiple Project 是 CocoaPods 1.7.0 引入的新功能,不只會生成 Pods\/Pods.xcodeproj,並且會爲每一個 Pod 單獨生成 Xcode Project,Pod Native Target 屬於獨立的 Pod Xcode Project,Pod Xcode Project 是 Pods.xcodeproj 的子工程,相比 Single Project Generator,會有性能優勢。這裏我們以 Single Project 爲例,來說明 Pods.xcodeproj 生成的一般流程。"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/a3\/a38b2f34d29f24d3dd8b31059d550364.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"iOSPlayground 工程在 Single Project Generator 下生成的工程結構:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/82\/827f747219b8ed65e153c9b1006f3f2b.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"embedcomp","attrs":{"type":"table","data":{"content":"
Pods\/沙盒目錄
Pods\/Pods.xcodeprojPod Target、Aggregate Target 的容器工程
Pods\/Manifest.lockPodfile.lock 的備份,項目構建前會和 Podfile.lock 比較,以判斷當前的沙盒和工程對應
Pods\/Headers\/管理 Pod 頭文件的目錄,支持基於 HEADER_SEARCH_PATHS 的頭文件檢索
Pods\/Target Support Files\/CocoaPods 爲 Pod Target、Aggregate Target 生成的文件,包括:xcconfig、modulemap、resouce copy script、framework copy scrpt 等"}}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"User 工程整合"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個流程的目標是將 Pods.xcodeproj 整合到 User.xcodeproj 上,將 User Target 整合到 CocoaPods 的依賴環境中,從而在後續的構建流程生效:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/09\/09c2f43e81b9922a4f0e0c3bd2214ac1.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"User Target 的整合一般包括 Xcconfig 整合、Target 整合、動態庫整合和資源整合等:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/a7\/a73fa550efc80d667a654c94ff321b00.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"embedcomp","attrs":{"type":"table","data":{"content":"
User.xcodeproj殼工程,用於生成 App 等產品,名字一般自定義
User Target殼工程中用於生成指定產品的 Target
User.xcworkspaceCocoaPods 生成,合併 User.xcodeproj 和 Pods\/Pods.xcodeproj"}}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"User 工程構建"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Pod Install 執行完成後,就將 User Target 整合到了 CocoaPods 環境中。User Target 依賴 Aggregate Target,Aggregate Target 依賴所有 Pod Targets,Pod Targets 按照 Pod 描述文件(Podspec)中的依賴關係進行依賴,這些依賴關係保證了編譯順序"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"iOSPlayground 工程中 User Target: iOSPlayground 依賴了 Aggregate Target 的產物 libPods-iOSPlayground.a"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/c3\/c31c804efbbe1e6958e552065f7ae1a6.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"iOSPlayground 工程中 Aggregate Target: Pod-iOSPlayground 依賴了了所有 Pod Targets"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/df\/dffd30e347982508c63108a7041bcabe.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"編譯完成後,就開始進行鏈接、資源整合、動態庫整合、APP 簽名等操作,直到最後生成完整 APP。Xcode 提供了 Build Phases 方便我們查看和編輯構建流程配置,同時我們也可以通過構建日誌查看整個 APP 的構建流程:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/44\/44a9b03dcf8c413872461e7519761a13.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"如何評估"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們需要建立一些數據指標來進行衡量我們的優化結果,CocoaPods 內置了 ruby-prof(https:\/\/ruby-prof.github.io\/) 工具。ruby-prof 是一個 Ruby 程序性能分析工具,可以用於測量程序耗時、對象分配以及內存佔用等多種數據指標,提供了 TXT、HTML、CallGrind 三種格式。首先安裝 ruby-prof,然後設置環境變量 COCOAPODS_PROFILE 爲性能測試文件的地址,Pod Install 執行完成後會輸出性能指標文件ruby-prof 提供的數據是我們進行 CocoaPods 效能優化的重要參考,結合這部分數據我們可以很方便地分析方法堆棧的耗時以及其他性能指標。"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/9e\/9e0ec911c73dff3f5d72f5d7a6b694ad.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是 Ruby-prof 工具是 Ru"},{"type":"text","marks":[{"type":"del"}],"text":"b"},{"type":"text","text":"y 方法級別,難以細粒度地查看實際 Pod Instal 過程中各個具體流程的耗時,可以作爲數據參考,但是難以作爲效率優化結果的標準。同時我們也需要一套體系來衡量 Pod Install 各個流程的耗時,基於這個訴求,我們自研了 CocoaPods 的 Profiler,並且在遠端搭建了數據監控體系:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"Profiler 可以在本地打印各階段耗時,也可以下鑽到詳細的流程"}]}]}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"install! consume : 5.376132s prepare consume : 0.002049s resolve_dependencies consume : 4.065177s download_dependencies consume : 0.001196s validate_targets consume : 0.037846s generate_pods_project consume : 0.697412s integrate_user_project consume : 0.009258s"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":2,"normalizeStart":2},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"Profiler 會把數據上傳到平臺,方便進行數據可視化"}]}]}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/c0\/c0bf93f0b0c0577ee8fe3b5e94ce3f1d.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Profiler 除了上傳 Pod Install 各個耗時指標以外,也會上傳失敗情況和錯誤日誌,這些數據會被用於衡量穩定性優化的效果。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"優化實踐"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對 Pod Install 的執行流程有了一定的瞭解後,基於 Ruby 語言的提供的動態性,我們開始嘗試在 0 侵入、不影響現有研發流程的前提下,改造 CocoaPods 做來解決我們遇到的問題,並且取得了一些收益。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Source 更新"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"按需更新"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們知道 CocoaPods 在進行依賴版本決議的時候,會從本地 Source 倉庫(一般是多個 Git 倉庫)中查找符合版本約束的 Podspecs,如果本地倉庫中沒有符合要求的,決議會失敗。倉庫中沒有 Podspec 分爲幾種情況:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"本地 Source 倉庫沒有更新,和遠程 Source 倉庫不同步"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"遠程 Source 倉庫沒有發佈符合版本約束的 Podspec"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原因 2 是符合預期的;原因 1 是因爲研發同學沒有主動更新本地 source repo 倉庫,可以在 pod install 後添加 --repo-update 參數來強制更新本地倉庫,但是每次都加上這個參數會導致 Pod Install 執行效率下降,尤其是對包含多個 source repo 的工程。"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"UI.section 'Updating local specs repositories' do\n  analyzer.update_repositories\nend if repo_update?\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"怎麼做可以避免這個問題,同時保證研發效率?"}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"不主動更新倉庫,如果找不到 Podspec,再自動更新倉庫"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"不更新所有倉庫,按需更新部分倉庫"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"如果有新增組件,找不到 Podspec 後,自動更新所有倉庫"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"如果部分更新後依然失敗,自動更新所有倉庫;這種情況出現在隱式依賴新增的情況"}]}]}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/48\/48157e37d77d48f2fcd143ce0f991e35.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"倉庫按需更新,是指基於 Podfile.lock 查找哪些依賴的版本不在所屬的倉庫內,標記該依賴所屬的倉庫爲需要更新,循環執行,檢查所有依賴,獲取到所有需要更新的倉庫,更新所有標記爲需要更新的倉庫。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這樣研發同學不需要關心本地 Source 倉庫是否更新,倉庫會按照最佳方式自動和遠程同步。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"更新同步"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在倉庫更新流程中也會出現併發問題,比如在抖音的 CI 環境上構建任務是併發執行的,在某些情況下多個任務會同時更新本地 source 倉庫,Git 倉庫會通過鎖同步機制強制併發更新失敗,這就導致了 CI 任務難以併發執行。如何解決併發導致的失敗問題?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"最簡單的方式就是避免併發,一個機器同時只能執行一個任務,但是這會導致 CI 執行效率下降。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"不同任務間進行 source 倉庫隔離,CocoaPods 默認提供了這種機制,可以通過環境變量 CP_REPOS_DIR 的設置來自定義 source 倉庫的根目錄,但是 source 倉庫隔離後,會導致同一個倉庫佔用多份磁盤,同時在需要更新的場景下,需要更新兩次,這會影響到 CI 執行效率。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"方案 1 和方案 2 一定程度保證了任務的穩定性,但是影響了研發效率,更好的方式是隻在需要同步的地方串行,不需要同步的地方併發執行。一個自然而然的想法就是使用鎖,不同 CocoaPods 任務是不同的 Ruby 進程,在進程間做同步可以使用文件鎖。通過文件鎖機制,我們保證了只有一個任務在更新倉庫。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"CocoaPods 倉庫更新流程流程遇到的問題,本質是由於使用了本地的 Git 倉庫來管理導致,在 CocoaPods 1.9.0 + ,引入 CDN Source 的概念,抖音也在嘗試向 CDN Source 做遷移。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"依賴決議"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"簡化決議"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"CocoaPods 的依賴版本決議流程是基於 "},{"type":"codeinline","content":[{"type":"text","text":"Molinillo"}]},{"type":"text","text":" 的,"},{"type":"codeinline","content":[{"type":"text","text":"Molinillo"}]},{"type":"text","text":" 是基於 DAG 來進行依賴解析的,通過構建圖可以方便的進行依賴關係查找、依賴環查找、版本降級等。但是使用圖來進行解析是有成本的,實際上大部分的本地依賴決議場景並不需要這麼複雜,Podfile.lock 中的版本就是決議後的版本,大部分的研發流程直接使用 Podfile.lock 進行線性決議就可以,這可以大幅加快決議速度。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"Specification 緩存"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"依賴分析流程中,CocoaPods 需要獲取滿足約束的 Specifications,1.7.5 上的流程是獲取一個組件的所有版本的 Specifications 並緩存,然後從 Specifications 中篩選出滿足約束的 Specifications。對於複雜的項目來說,往往對一個依賴的約束來自於多個組件,比如 A 依賴 F(>=0),B 依賴 F (>=0),在分析完 A 對 F 的依賴後,在處理 B 對 F 的依賴時,還是需要進行一次全量比較。通過優化 Specification 緩存層可以減少這部分耗時,直接返回。"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"module Pod::Resolver\n  def specifications_for_dependency(dependency, additional_requirements = [])\n    requirement = Requirement.new(dependency.requirement.as_list + additional_requirements.flat_map(&:as_list))\n    find_cached_set(dependency).\n      all_specifications(warn_for_multiple_pod_sources).\n select { |s| requirement.satisfied_by? s.version }.\n      map { |s| s.subspec_by_name(dependency.name, false, true) }.\n      compact\n  end\nend\n\nmodule Pod::Specification::Set\n  def all_specifications(warn_for_multiple_pod_sources)\n     @all_specifications ||= begin\n      #...\n    end\n  end\nend\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"優化後:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"module Pod::Resolver\n  def specifications_for_dependency(dependency, additional_requirements = [])\n    requirement_list = dependency.requirement.as_list + additional_requirements.flat_map(&:as_list)\n    requirement_list.uniq!\n    requirement = Requirement.new(requirement_list)\n    find_cached_set(dependency).\n      all_specifications(warn_for_multiple_pod_sources, requirement) .\n      map { |s| s.subspec_by_name(dependency.name, false, true) }.\n      compact\n  end\nend\n\nmodule Pod::Specification::Set\n  def all_specifications(warn_for_multiple_pod_sources, requirement)\n    @all_specifications ||= {}\n    @all_specifications[requirement]  ||= begin\n      #...\n    end\n  end\nend\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"CocoaPods 1.8.0 開始也引入了這個優化,但是 1.8.0 中並沒有重載 Pod::Requirement 的 eql? 方法,這會導致使用 Pod::Requirement 對象做 Key 的情況下,沒有辦法命中緩存,導致緩存失效了,我們重載 eql? 生效決議緩存,加速了 Molinillo 決議流程,獲得了很大的性能提升:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"module Pod::Requirement\n  def eql?(other)\n    @requirements.eql? other.requirements\n  end\nend\n"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"循環依賴發現"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當出現循環依賴時,CocoaPods 會報錯,但報錯信息只有誰和誰之間存在循環依賴,比如:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"There is a circular dependency between A\/S1 and D\/S1"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"隨着工程的複雜度提高,對於複雜的循環依賴關係,比如 A\/S1 -> B -> C-> D\/S2 -> D\/S1 -> A\/S1, 基於上面的信息我們很難找到真正的鏈路,而且循環依賴往往不止一條,subspec、default spec 等設置也提高了問題定位的複雜度。我們優化了循環依賴的報錯,當出現循環依賴的時候,比如 A 和 D 之間有"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"環,我們會查找 A -> D\/S1 之前所有的路徑,並打印出來:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"There is a circular dependency between A\/S1 and D\/S1 Possible Paths:A\/S1 -> B -> C-> D\/S2 -> D\/S1 -> A\/S1 A\/S1 -> B -> C -> C2 -> D\/S2 -> D\/S1 -> A\/S1 A\/S1 -> B -> C -> C3 -> C2 -> D\/S2 -> D\/S1 -> A\/S1"}]}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"沙盒分析緩存"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SandboxAnalyzer 主要用於分析沙盒,通過決議結果和沙盒內容判斷哪些 Pods 需要刪除哪些 Pods 需要重裝,但是在分析過程中,存在大量的重複計算,我們緩存了 sandbox analyzer 計算的中間結果,使 sandbox analyzer 流程耗時減少 60%。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"依賴下載"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大型項目往往要引入幾百個組件,一旦組件發佈新版本或者沒有命中緩存就會觸發組件下載,依賴下載慢也成爲大型項目反饋比較集中的問題。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"依賴併發下載"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"CocoaPods 一個很明顯的問題就是依賴是串行下載的,串行下載難以達到帶寬峯值,而且下載過程除了網絡訪問,還會進行解壓縮、文件準備等,這些過程中沒有進行網絡訪問,如果把下載並行是可以提高依賴下載效率的。我們將抖音的下載過程優化爲併發操作,下載流程總時間減少了 60%以上。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"HTTP API 下載"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"CocoaPods 支持多種下載方式的,比如 Git、Http 等。一般組件以源碼發佈,會使用 Git 地址作爲代碼來源,但是 Git 下載是比 Http 下載慢的,一是 Git 下載需要做額外的處理和校驗,速度和穩定性要低於 HTTP 下載,二是在組件是通過 Git 和 Commit 指明 source 發佈的情況下,Git 下載頁會克隆倉庫的日誌 GitLog, 對於開發比較頻繁的項目,日誌大小要遠大於倉庫實際大小,這會導致組件下載時間變長。我們基於 Gitlab API 將 Git 地址轉化爲 HTTP 地址進行下載,就可以加快這部分組件的下載速度了。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"沙盒軟連接"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"CocoaPods 在安裝依賴的時候,會在沙箱 Pods 目錄下查找對應依賴,如果對應依賴不存在,則會將緩存中的依賴文件拷貝到沙箱 Pods 目錄下。對於本地有多個工程的情況,Pods 目錄佔用磁盤就會更多。同時,將緩存拷貝到沙箱也會耗時,對於抖音工程,如果所有的內容都要從緩存拷貝到沙箱,大概需要 60s 左右。我們使用軟連接替換拷貝,直接通過鏈接緩存中的 Pod 內容來使用依賴,而不是將緩存拷貝到 Pods 沙箱目錄中,從而減少這部分磁盤佔用,同時減少拷貝的時間。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"緩存有效檢查"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在抖音使用 CocoaPods 的過程中,尤其是 CI 併發環境,存在緩存中文件不全的情況,缺少部分文件或者整個文件夾,這會導致編譯失敗或者運行存在問題。CocoaPods 本身有保證 Pods 緩存有效的機制:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/9c\/9c1a850f746a21864b69a71fc3b69db7.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"依賴Podspec寫入緩存"}]},{"type":"text","text":" 這一步實際上是爲了保證整個緩存流程是有效的:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"def path_for_spec(request, slug_opts = {})\n  path = root + 'Specs' + request.slug(slug_opts)\n  path.sub_ext('.podspec.json')\nend\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是在 "},{"type":"codeinline","content":[{"type":"text","text":"依賴Podspec寫入緩存"}]},{"type":"text","text":" 中,CoocoPods 存在 BUG。"},{"type":"codeinline","content":[{"type":"text","text":"path.sub_ext('.podspec.json')"}]},{"type":"text","text":"會導致部分版本信息被錯誤地識別爲後綴名,比如 XXX 0.1.8-5cd57.podspec.json 版本寫入到緩存中變爲 0.1.podspec.json, 丟失了小版本和內容標示信息,會導致了整個 Pod 緩存有效性校驗失效。比如 XXX 0.1.8 緩存執行成功,XXX 0.1.9 在緩存 copy、prepare 的流程被取消,實際上很大概率上 XXX 0.1.9 的緩存是不完整的,但是下次執行的時候,緩存目錄存在,Podspec 存在(0.1.podspec.json),不完整的緩存被判定爲有效,使用了錯誤的緩存,導致了編譯失敗。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"修改 path_for_spec 邏輯,保證依賴 Podspec 緩存寫入到正確的文件 0.1.8-5cd57.podspec.json,而不是 0.1.podspec.json。"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"def path_for_spec(request, slug_opts = {})\n  path = root + 'Specs' + request.slug(slug_opts)\n  Pathname.new(path.to_path + '.podspec.json')\nend\n"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"依賴下載同步"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在緩存下載的環境,依然會出現併發問題,我們通過對 Pod 下載流程加文件鎖的機制來保證併發下下載任務的穩定。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Pods 工程生成"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"增量安裝"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"CocoaPods 在 1.7.0+ 提供了新的 Pods Project 的生成策略:Multiple Project Generator。通過開啓多 Project「generate_multiple_pod_projects」,可以提高 Xcode 工程的檢索速度。在開啓多 Project 的基礎上,我們可以開啓增量安裝「incremental_installation」,這樣在 Pods 工程生成的時候,會基於上次 Pod Install 的緩存按需生成部分 Pod Target 而不會全量生成所有 Pod Target,對二次 Pod Install 的執行效率改善很明顯,以抖音爲例,二次 Pod Install (增量)是首次 Pod Install (全量)的 40%左右。這個是 CocoaPods 的 Feature,就不展開說明了。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"單 Target\/Configuration 安裝"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大部分工程會包含多個業務 Target 和 Build Configuration,Pod Install 會對所有的 Target 進行安裝,對所有的 Build Configuration 進行配置。但是實際本地開發過程中一般只會使用一個 Build Configuration 下的一個 Target,其他 Target 和 Configuratioins 的依賴安裝實際上是冗餘操作。比如有些依賴只有某幾個 Target 有,如果全量安裝,即使不使用這些 Target,也要下載和集成這些依賴。抖音工程包括多個業務 Target 和多個構建 Build Configuration,不同業務 Target 之間依賴的差集有幾十個,只對特定 Target 和特定的 Configuration 進行集成能夠獲得比較明顯的優化,這個方案落地後:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"Pod Install 安裝依賴數量減少,決議時間、Pod 工程生成時間減少;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"單 Target\/Configuration 下 Pod 工程複雜度減少, Xcode 索引速度改善明顯,以抖音爲例子,索引耗時減少了 60%;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"可以爲每個 Target、每個 Configuration 配置獨立的依賴版本;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"每個 Target 的編譯隔離,避免了其他 Target 的依賴影響當前 Target 的編譯。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Pod 是全量 Target 安裝,在編譯的時候並沒有對非當前 Target 的依賴做完整的隔離,而是在鏈接的時候做了隔離,但是 OC 的方法調用是消息轉發機制的,因此沒有鏈接指定庫的問題被延遲到了運行時才能發現 (unrecognized selector)。使用單 Target 的方式可以提前發現這個類問題。"}]}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"緩存 FileAccessors"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 Pods 工程生成流程中有三個流程會比較耗時,這些數據每次 Pod Install 都需要重新生成:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Pod 目錄下的文件和目錄列表,需要對目錄下的所有節點做遍歷;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Pod 目錄下的動態庫列表,需要分析二進制格式,判斷是否爲動態庫;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Pod 文件的訪問策略緩存 glob_cache,這個 glob_cache 是用於訪問組件倉庫中不同類型文件的,比如 source files、headers、frameworks、bundles 等。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但其實這些數據對固定版本的依賴都是唯一的,如果可以緩存一份就可以避免二次生成導致的額外耗時,我們補充了這個緩存層,以抖音爲例子,使 Pod Clean Install 減少了 36%,Pod No-clean Install 減少了 42%。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"添加 FileAccessors 緩存層後,在效率上獲得提升的同時,在穩定性上也獲得了提升。因爲在本地記錄了 Pod 完整的文件結構,因此我們可以對 Pod 的內容做檢查,避免 Pod 內容被刪除導致構建失敗。比如研發同學誤刪了緩存中的二進制庫,CocoaPods 默認是難以發現的,需要延遲到鏈接階段報 Symbol Not Found 的錯誤,但是基於 FileAccessors 緩存層,我們可以在 Pod Install 流程對 Pod 內容做檢查,提前暴露出二進制庫缺失,觸發重新下載。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"提高編譯併發度"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Pod Target 的依賴關係會保證 Target 按順序編譯,但是會導致 Target 編譯的併發度下降,一定程度上降低了編譯效率。其實生成靜態庫的 Pod Target 不需要按順序進行編譯,因爲靜態庫編譯不依賴產物,只是在最後進行鏈接。通過移除靜態庫的 Pod Target 對其他 Target 的依賴,可以提高整體的編譯效率。"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/d6\/d68050f82510c49006c4340540a53ce1.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 Multi Project 下,「Dependency Subproject」會導致索引混亂,移除靜態庫的 Pod Target 對其他 Target 的依賴後,我們也可以刪除 Dependent Pod Subproject,減少 Xcode 檢索問題。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"Arguments Too Long"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"超大型工程在編譯時穩定性降低,往往會因爲工程放置的目錄長產生一些未定義錯誤,其中錯誤比較大的來源就是 Arguments Too Long,表現爲:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Build operation failed without specifying any errors ;Verify final result code for completed build operation"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"根本原因是依賴數目過多導致編譯\/鏈接\/打包流程的環境變量總數過多,從而導致命令長度超過 Unix 的限制,在構建流程中表現爲各種不符合預期的錯誤,具體可以見https:\/\/github.com\/CocoaPods\/CocoaPods\/issues\/7383。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其實整個構建流程的環境變量主要來源於系統 和 Build Settings,系統環境一般是固定的,影響比較大的就是 Build Settings 裏的配置,其中影響最大的是:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"編譯參數"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"GCC_PREPROCESSOR_MACRO 預編譯宏"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"HEADER_SEARCH_PATHS 頭文件查找路徑"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"鏈接參數"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"FRAMEWORK_SEARCH_PATHS FRAMEWORK 查找路徑"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"LIBRARY_SEARCH_PATHS LIBRARY 查找路徑"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"OTHER_LDFLAGS 用於聲明連接參數,包括靜態庫名稱"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個比較直接的解決方案就是縮短工程目錄路徑長度來臨時解決這個問題,但如果要徹底解決,還是要徹底優化 Build Setting 參數的複雜度,減少依賴數量可能會比較難,一個比較好的思路就是優化參數的組織方式。"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"GCC_PREPROCESSOR_MACRO,在殼工程拆分掉業務代碼後,注入到 User Target 的預編譯宏可以逐步廢棄;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"HEADER_SEARCH_PATHS 會引入所有頭文件的目錄作爲 Search Path,這部分長度會隨着 Pod 數目的增加不斷增長,導致構建流程變量過長,從而讓阻塞打包。我們基於 HMAP 將 Header Search Path 合併成一個來減少 Header Search Path 的複雜度。除了用於優化參數長度外,這個優化的主要用途是可以減少 header 的查找複雜度,從而提高編譯速度,我們在後續的系列文章會介紹。"}]}]}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"HEADER_SEARCH_PATHS = $(inherited) \"${PODS_ROOT}\/Headers\/hmap\/37727fabd99bae1061668ae04cfc4123\/Compile_Public.hmap\"\n"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"鏈接參數:FRAMEWORK_SEARCH_PATHS、LIBRARY_SEARCH_PATHS、OTHER_LDFLAGS 聲明是爲了給鏈接器提供可以查找的靜態庫列表。OTHER_LDFLAG S 提供 filelist 的方式來聲明二進制路徑列表,filelist 中是實際要參與鏈接的靜態庫路徑,這樣我們就可以三個參數簡化爲 filelist 聲明,從而減少了鏈接參數長度。除了用於優化參數長度外,這個優化的主要用途是可以減少靜態庫的查找複雜度,從而提高鏈接速度,我們在後續的系列文章會介紹。"}]}]}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"OTHER_LDFLAGS[arch=*] = $(inherited) -filelist \"xx-relative.filelist,${PODS_CONFIGURATION_BUILD_DIR}\"\n"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"研發流程"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"新增文件"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"組件化的一個目標是業務代碼按架構設計拆分成組件 Pod。但如果在一個組件中新增文件,比如在組件 A 中新增文件,依賴組件 A 的組件 B 是不能直接訪問新增文件的頭文件的,需要重新執行 Pod Install,這樣會影響整體的研發效率。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲什麼組件 B 不能夠訪問組件 A 的新增文件?在 Pod Install 後,組件 A 公共訪問的頭文件被索引在 Pods\/Headers\/Public\/A\/ 目錄下,組件 B 的 HEADER_SEARCH_PATH 中配置了 Pods\/Headers\/Public\/A\/,因此就可以在組件 B 的代碼裏引入組件 A 的頭文件。新增頭文件的頭文件沒有在目錄中索引,所以組件 B 就訪問不到了。只需要在添加文件後,建立新增頭文件的索引到 Pods\/Headers\/Public\/A\/目錄下,就可以爲組件 B 提供組件 A 新增文件的訪問能力,這樣就不需要重新 Pod Install 了。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"Lockfile 生成"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在依賴管理的部分場景中,我們只需要進行依賴決議,重新生成 Podfile.lock,但通過 Pod Install 生成是需要執行依賴下載及後續流程的,這些流程是比較耗時的,爲了支持 Podfile.lock 的快速生成,可以對 install 命令做了簡化,在依賴決議後就可以直接生成 Podfile.lock:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"class Pod::Installer\n  def quick_generate_lockfile!\n    # 初始化 sandbox 環境\n    quick_prepare_env\n    quick_resolve_dependencies\n    quick_write_lockfiles\n  end\nend\n"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"總結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"CocoaPods 的整體優化方案以 RubyGem 「seer-optimize」 的方式輸出,對 CocoaPods 代碼 0 侵入,只要接入 seer-optimize 就可以生效,目前在字節內部已經被十幾個產品線使用了:抖音、頭條、西瓜、火山、多閃、瓜瓜龍等,執行效率和穩定性上都獲得了明顯的效果。比如抖音接入 optimize 開啓相關優化後,全量 Pod Install 耗時減少 50%,增量 Pod Install 平均耗時減少 65%。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"seer-optimize 是抖音 iOS 工程化解決方案 Seer 的的一部分,Seer 致力於解決客戶端在依賴管理和研發流程中遇到的問題,改善研發效率和穩定性,後續會逐步開源,以改善 iOS 的研發體驗。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文轉載自:字節跳動技術團隊(ID:toutiaotechblog)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文鏈接:"},{"type":"link","attrs":{"href":"https:\/\/mp.weixin.qq.com\/s\/Pt6pcxKCHhdnnWPYrToNvA","title":"xxx","type":null},"content":[{"type":"text","text":"抖音研發效能建設 - CocoaPods 優化實踐"}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章