天天看點

知乎 iOS 基于 CocoaPods 實作的二進制化方案

作者:閃念基因

背景

随着公司業務規模的增長,iOS 用戶端的代碼量也越來越大,編譯一次項目的時間也越來越長。減少編譯時間成了一個不得不面對的問題。

現有的二進制方案如 Carthage、Rome 等都是在本地生成 framework,沒法實作「一次編譯,處處使用」的目标。

為了實作這個目标,就需要一個人或者一個 CI Job,把編譯好的二進制産物上傳到某個的地方,集中化地管理這些二進制形式的依賴。然後在每個人 pod install 的時候,檢查該 pod 版本對應的二進制是否存在,如果有就使用,沒有就繼續采用源碼的方式依賴。

上面的方案隐藏了許多細節,比如到底應該如何集中管理這些 pod,如何知道對應的版本是否存在,如何在 pod install 的時候動态地把這些 pod 從源碼形式的依賴換成二進制形式的依賴等等。

因為這整個流程涉及生産方(産生二進制)和消費方(使用二進制),我就把整個方案分為兩個流程詳細介紹一下。

産生二進制

代碼結構

産生二進制的流程在一個 CI Job 中,每隔一段時間,它會同步主倉庫最新的 dev 分支,然後運作管理此環節的工具,Platypus。

它的結構如下,

知乎 iOS 基于 CocoaPods 實作的二進制化方案

Platypus 代碼結構

config.yml 是與工程相關聯的配置,其中包含了需要二進制化的名單(pod_names),project 檔案相關資訊,以及工程初始化的 action(prebuild_action)等等。specs_repo 是私有的 podspec 倉庫,需要單獨建立,負責集中管理已經二進制好的 pod 資訊。

具體流程

如下圖,

知乎 iOS 基于 CocoaPods 實作的二進制化方案

二進制産生流程

下面是各個步驟的詳細說明,

  • a) 對于大多數項目來說是 pod install,但如果在不改變 podfile 原有寫法的基礎上實作此套方案,需要把使用 patch 過後的 pod install 方式,這個在使用二進制這個部分會詳細說明。
  • b) 白名單存在的意義有兩點,一是有些 pod 本來就是二進制好了的;二是某些 pod 因為頭檔案沒有用 < > 的方式引用在目前階段沒法二進制,否則就會因為找不到頭檔案編譯失敗。
  • c) 模拟器和真機的版本都需要編譯,最終使用 lipo 把兩份二進制合并到一個 .framework 中。如果 pod 中包含 Swift 代碼,需要把模拟器和真機的編譯産物中的 swiftdoc 和 swiftmodule 都合并到一個檔案夾中。由于 Swift 版本的原因,由舊版 Xcode 編譯生成 Swift 二進制是無法在新版 Xcode 中使用的。
  • f) 通過 CocoaPods 中的 Analyzer 調用 analyzer.analyze.specifications,可以擷取目前項目所有依賴 pod 的 podspecs,具體操作可以看這一篇文章。關于如何編輯 podspec,可以使用這個 gem。編輯的内容包括删掉 source_files 字段,把 vendored_frameworks 字段指向 .framework, source 指向上傳生成的 URL, resources 指向對應 .framework 中的資源等等。儲存後,作為二進制時依賴使用的 podspec。
  • g) 這一步是為了把項目中依賴的 pod 版本與二進制化後的版本建立起聯系。因為項目中依賴的引用方式五花八門,有用 CocoaPods Master Repo 中版本号的,有用 git tag 的,也有用 git commmit 的,針對不同的引用方式,都要有對應的比對規則,

比如有一個使用 tag 方式引用的元件,把它的 tag 号後面加上 -zhihu-static 作為它在私有 Specs 倉庫中的版本号,它在 podfile 中的 external_source 作為 summary 字段,同時確定唯一性。這裡的映射關系隻要能一一對應起來,随便怎麼建立都好。

pod 'A', git: '[email protected]:xxx/A.git', tag: '4.24.0.9'           

是以它被改完版本号後 poddpec 會長成這個樣子,

{
  "name": "A",
  "version": "4.24.0.9-zhihu-static",
  "summary": "{:git=>\"[email protected]:xxx/A.git\", :tag=>\"4.24.0.9\"}"
  ...
}           
  • h) Specs 倉庫目錄結構如下所示,目錄均為手動建立,沒有使用 CocoaPods 提供的方式更新。
├── A
│   └── 4.22.0.8-zhihu-static
│       └── A.podspec.json
├── B
│   ├── 0.2.21-zhihu-static
│   │   └── B.podspec.json
│   └── 0.2.9-zhihu-static
│       └── B.podspec.json
├── C
│   └── 1.4.0-zhihu-static
│       └── C.podspec
└── D
    └── 2.5.0-zhihu-static
        └── D.podspec           

使用二進制

在觸發 pod install 過程之前,需要在本地把私有 Specs 倉庫更新到最新,pod repo update xxx。

接下來就是 patch pod install 替換依賴的過程了。在不更改 podfile 的情況下,隻能模仿 pod install 的過程,自己建立一個腳本來替代這個操作了。整個過程不複雜,可以參考下面這一段帶注釋的代碼,

# 參考 `CocoaPods` 的源碼,模拟 `pod install` 執行的過程
argv = CLAide::ARGV.new([])
cmd = Pod::Command.new(argv)
cmd.send :verify_podfile_exists!
installer = cmd.send :installer_for_config
installer.repo_update = false
installer.update = false

podfile = installer.podfile

# 擷取此次 install 的配置,是全部使用二進制還是全部使用源碼
# 全部使用二進制時,哪些 pod 依舊使用源碼引入
use_all_binary, source_pod_list = ZHPodInstallHelper.read_binary_pods_pref
use_all_binary = false if ENV['ALL_SOURCE'] == 'true'
unless use_all_binary
  puts '  pod install with all source'
  installer.install!
  exit(0)
end

# 為 podfile 添加二進制 Specs 倉庫的 source
podfile.send(:get_hash_value, 'sources')
hash_sources = podfile.send(:get_hash_value, 'sources') || []
hash_sources << '[email protected]:xxx/E.git'
podfile.send(:set_hash_value, 'sources', hash_sources.uniq)

# 周遊 podfile 中的所有 dependencies
podfile.root_target_definitions.each do |root_target_definition|
  children_definitions = root_target_definition.recursive_children
  children_definitions.each do |children_definition|
    dependencies_hash_array = children_definition.send(:get_hash_value, 'dependencies')
    next if dependencies_hash_array.count.zero?
    dependencies_hash_array.each do |dependencie_hash_item|
      next if dependencie_hash_item.class.name != 'Hash'
      dependencie_hash_item.each do |name, value|
        next if value[0].is_a?(Hash) && value[0][:path]
        search_name = name
        search_name = name.split('/')[0] if name.include?('/')

        # 對于想要以源碼依賴的 pod,不作修改
        next if source_pod_list.include?(search_name)

        # 根據 podfile 中引用的源碼版本,在私有 Specs 倉庫中查找相應二進制的版本
        version = ZHPodInstallHelper.get_binary_version(search_name, value[0].to_s)
        # 存在對應的二進制版本,就替換掉
        dependencie_hash_item[name] = [version] if version
      end
    end
    # 替換 podfile 的 dependencies 為修改後的 dependencies
    children_definition.send(:set_hash_value, 'dependencies', dependencies_hash_array)
  end
end

installer.install!           

限制

說完了整個方案的流程,我們再來談談這個方案存在的一些問題,

  • 需要自定義 pod install 過程,同時修改某些 CocoaPods 中的私有屬性
  • 最終的 binary size 會比使用源碼的時候大一點,不建議最終上傳 Store 的時候使用
  • 缺少一個驗證的機制,如果已釋出的二進制包不能被項目正常引用,那麼會導緻所有人的編譯失敗
  • 由于工程采用的是全部靜态庫依賴的形式,是以在二進制和源碼切換的過程中會對 project 檔案産生更改

總結

以上就是知乎 iOS 用戶端二進制預編譯的方案,有任何問題都歡迎大家留言,寫下你的看法

作者:Xinyu

出處:https://zhuanlan.zhihu.com/p/44280283