CocoaPods使用指南

語言: CN / TW / HK

theme: awesome-green

前言

對於大多數軟體開發團隊來說,依賴管理工具必不可少,它能針對開源和私有依賴進行安裝與管理,從而提升開發效率,降低維護成本。針對不同的語言與平臺,其依賴管理工具也各有不同,例如 npm 管理 Javascript、Gradle 、Maven 管理 Jar 包、pip 管理 Python 包,Bundler、RubyGems 等等。本文聚焦於 iOS 方面,對 CocoaPods 的使用和部分原理進行闡述。

簡單易用的 CocoaPods

對於 iOSer 來說,CocoaPods 並不陌生,幾乎所有的 iOS 工程都會有它的身影。CocoaPods 採用 Ruby 構建,它是 Swift 和 Objective-C Cocoa 專案的依賴管理工具。在 MacOS 上,推薦使用預設的 Ruby 進行安裝 (以下操作均在 CocoaPods 1.10.1、Ruby 2.7.2 進行): shell sudo gem install cocoapods 如果安裝成功,便可以使用 pod 的相關命令了。針對一個簡單的專案來說,只需三步便可引入其他的依賴: 1. 建立 Podfile 檔案( CocoaPods 提供了 pod init 命令建立) 2. 對 Podfile 檔案進行編寫,新增依賴的庫,版本等資訊。 3. 在命令列執行 pod install 命令

順利的話,這時在專案目錄下會出現以下檔案:

  • .xcworkspace:CocoaPods 將專案分為了主工程與依賴工程(Pods)。與 .xcodeproj 相比 .xcworkspace 對於管理多個專案的能力更強,你也可以將複雜的大型應用轉換為以 .xcworkspace 構建的多個兄弟專案,從而更輕鬆的維護和共享功能。
  • Podfile.lock:記錄並跟蹤依賴庫版本,將依賴庫鎖定於某個版本。
  • Pods 資料夾:存放依賴庫程式碼。
  • Pods/Manifest.lock:每次 pod install 時建立的 Podfile.lock 的副本,用於比較這兩個檔案。一般來說,Podfile.lock 會納入版本控制管理,而 Pods 資料夾則不會納入版本控制變更;這意味著 Podfile.lock 表示專案應該依賴的庫版本資訊,而 Manifest.lock 則代表本地 Pods 的依賴庫版本資訊。在 pod install 後會將指令碼插入到 Build Phases,名為 [CP] Check Pods Manifest.lock,從而保證開發者在執行 app 之前能夠更新 Pods,以確保程式碼是最新的。

pod install vs. pod update

  • pod install:在每一次編輯 Podfile 以新增、更新或刪除 pod 時使用。它會下載並安裝新的 Pod,並將其版本資訊寫入 Podfile.lock 中。
  • pod outdated:列出所有比 Podfile.lock 中當前記錄的版本 newer 版本的 pod。
  • pod update [PODNAME]:CocoaPods 會查詢 newer 版本的 PODNAME,同時將 pod 更新到可能的最新版本(須符合 Podfile 限制)。若沒有 PODNAME,則會將每一個 pod 更新到可能的最新版本。

一般來說,每次編輯 Podfile 時使用 pod install,僅在需要更新某個 pod 版本(所有版本)時才使用 pod update。同時,需提交 Podfile.lock 檔案而不是 Pods 資料夾來達到同步所有 pod 版本的目的。

ps: newer 代表更加新的,若採用中文理解起來比較彆扭。

Podfile 語法規範

Podfile 描述了一個或多個 Xcode 專案的 target 依賴關係,它是一種 DSL,瞭解它對我們使用好 CocoaPods 是一個必不可少的步驟。下面列出其相關的語法規範:

Root Options

install!:指定 CocoaPods 安裝 Podfile 時使用的安裝方法和選項。如:

ruby install! 'cocoapods',          :deterministic_uuids => false,          :integrate_targets => false * :clean:根據 podspec 和專案支援平臺的指定,清理所有不被 pod 使用的檔案,預設為 true。 * :deduplicate_targets:是否對 pod target 進行重複資料刪除,預設為 true。 * :deterministic_uuids:建立 pod project 是否產生確定性 UUID,預設為 true。 * :integrate_targets:是否繼承到使用者專案中,為 false 會將 Pod 下載並安裝到到 project_path/Pods 目錄下,預設為 true。 * :lock_pos_sources:是否鎖定 pod 的原始檔,當 Xcode 嘗試修改時會提示解鎖檔案,預設為 true。 * :warn_for_multiple_pod_sources:當多個 source 包含同名同版本 pod 時是否發出警告,預設為 true。 * :warn_for_unused_master_specs_repo:如果沒有明確指出 master specs repo 的 git 是否發出警告,預設為 true。 * :share_schemes_for_development_pods:是否為開發中的 pod 分享 schemes,預設為 false。 * :disable_input_output_paths:是否禁用 CocoaPods 指令碼階段的輸入輸出路徑(Copy Frameworks 和 Copy Resources),預設為 false。 * :preserve_pod_file_structure:是否保留所有 pod 的檔案結構,預設為 false。 * :generate_multiple_pod_projects:是否為每一個 pod target 生成 一個 project,生成與 Pods/Pods 資料夾中,預設為 false。 * :incremental_installation:僅對自上次安裝的 target 與其關聯的 project 的變更部分進行重新生成,預設為 false。 * :skip_pods_project_generation:是否跳過生成 Pods.xcodeproj 並僅進行依賴項解析與下載,預設為 false。 ensure_bundler!:當 bundler 版本不匹配時發出警告。 ruby ensure_bundler! '~> 2.0.0'

Dependencies

pod:指定專案的依賴項

  • 依賴版本控制:=、>、>=、<、<= 為字面意思;~> 0.1.2 表示 0.1.2 <= currVersion < 0.2 之間的符合要求的最新版本版本。
  • Build configurations:預設依賴安裝在所有的構建配置中,但也可僅在指定構建配置中啟用。
  • Modular Headers:用於將 pod 轉換為 module 以支援模組,這時在 Swift 中可以不用藉助 bridging-header 橋接就可以直接匯入,簡化了 Swift 引用 Objective-C 的方式;也可以採用 use_modular_headers! 進行全域性的變更。
  • Source:指定具有依賴項的源,同時會忽略全域性源。
  • Subspecs:預設會安裝所有的 subspecs,但可制定安裝某些 subspecs。
  • Test Specs:預設不會安裝 test specs,但可選擇性安裝 test specs。
  • Local path:將開發的 pod 與其客戶端一起使用,可採用 path。
  • 指定某個特殊或者更為先進的 pod 版本

```ruby

依賴版本控制

pod 'Objection', '~> 0.9' 

Build configurations

pod 'PonyDebugger', :configurations => ['Debug', 'Beta'] 

Modular Headers

pod 'SSZipArchive', :modular_headers => true 

Source

pod 'PonyDebugger', :source => 'https://github.com/CocoaPods/Specs.git'

Subspecs

pod 'QueryKit', :subspecs => ['Attribute', 'QuerySet'] 

Test Specs

pod 'AFNetworking', :testspecs => ['UnitTests', 'SomeOtherTests']

Local path

pod 'AFNetworking', :path => '~/Documents/AFNetworking'

指定某個特殊或者更為先進的 Pod 版本

pod 'AFNetworking', :git => 'https://github.com/gowalla/AFNetworking.git', :branch => 'dev' pod 'AFNetworking', :git => 'https://github.com/gowalla/AFNetworking.git', :tag => '0.7.0' pod 'AFNetworking', :git => 'https://github.com/gowalla/AFNetworking.git', :commit => '082f8319af'

指定某個 podspec

pod 'JSONKit', :podspec => 'https://example.com/JSONKit.podspec' ```

inherit:設定當前 target 的繼承模式。

:complete 繼承父級 target 的所有行為,:none 不繼承父級 target 的任何行為,:search_paths 僅繼承父級的搜尋路徑。 ruby target 'App' do   target 'AppTests' do     inherit! :search_paths   end end target:與 Xcode 中的 target 相對應,block 中是 target 的依賴項。

預設情況下,target 包含在父級 target 定義的依賴項,也即 inherit!:complete。關於 :complete:search_paths:complete 會拷貝父級 target 的 pod 副本,而 :search_paths 則只進行 FRAMEWORK_SEARCH_PATHSHEADER_SEARCH_PATHS 的相關拷貝,具體可通過比對 Pods/Target Support Files 的相關檔案得以驗證,一般在 UnitTests 中使用,以減少多餘的 install_framework 過程。 ruby target 'ShowsApp' do   pod 'ShowsKit'   # 擁有 ShowsKit 和 ShowTVAuth 的拷貝   target 'ShowsTV' do     pod 'ShowTVAuth'   end   # 擁有 Specta 和 Expecta 的拷貝   # 並且能夠通過 ShowsApp 進行訪問 ShowsKit, 相當於 ShowsApp 是 ShowsTests 的宿主APP   target 'ShowsTests' do     inherit! :search_paths     pod 'Specta'     pod 'Expecta'   end end

abstract_target:定義 abstract_target,方便 target 進行依賴繼承,在 CocoaPods 1.0 版本之前為 link_with

ruby abstract_target 'Networking' do   pod 'AlamoFire'   target 'Networking App 1'   target 'Networking App 2' end abstract:表示當前 target 是抽象的,不會連結到 Xcode 的 target 中。

script_phase:新增指令碼階段。

在執行完 pod install 之後 CocoaPods 會將指令碼新增到對應的 target build phases

ruby target 'App' do script_phase { :name => 'scriptName' # 指令碼名稱,         :script => 'echo "nihao"' # 指令碼內容,         :execution_position => :before_compile / :after_compile         :shell_path => '/usr/bin/ruby' # 指令碼路徑         :input_files => ['/input/filePath'], # 輸入檔案         :output_files => ['/outpput/filePath'] # 輸出檔案 } end

Target configuration

platform:指定其構建平臺。

預設值為 iOS 4.3、OSX 10.6、tvOS 9.0 和 watchOS 2.0。CocoaPods 1.0 之前的版本為 xcodeproj

ruby platform :ios, '4.0' project:指定包含 target 的 Xcode project。這一般在 workspace 存在多個 xcode project 中使用:

```ruby

在 FastGPS Project 中可以找到一個名為 MyGPSApp 的 target

target 'MyGPSApp' do   project 'FastGPS'   ... end **inhibit_all_warnings**!:禁止所有警告。如果針對單個 Pod,則可以採用:ruby pod 'SSZipArchive', :inhibit_warnings => true pod 'SSZipArchive', :inhibit_warnings => true **user_modular_headers**!:將所有 Pod 模組化。如果針對單個 Pod,則可以採用:ruby pod 'SSZipArchive', :modular_headers => true pod 'SSZipArchive', :modular_headers => false **user_frameworks**!:採用 framework 而不是 .a 檔案的靜態庫。 可以通過:linkage指定使用靜態庫還是動態庫:ruby use_frameworks!:linkage => :dynamic / :static **supports_swift_versions**:指定 target definition 支援的 swift 版本要求ruby supports_swift_versions '>= 3.0', '< 4.0' ```

Workspace

workspace:指定包含所有專案的 Xcode workspace。 ruby workspace 'MyWorkspace'

Sources

sources:Podfile 從指定的源列表中進行檢索。sources 預設儲存在 ~/.cocoapods/repos 中,是全域性的而非按 target definition 儲存。當有多個相同的 Pod 時,優先採用檢索到的 Pod 的第一個源,因此當指定另一個來源時,則需顯示指定 CocoaPods 的源。 ruby source 'https://github.com/artsy/Specs.git' source 'https://github.com/CocoaPods/Specs.git'

Hooks

plugin:指定在安裝期間使用的外掛。 ruby plugin 'cocoapods-keys', :keyring => 'Eidolon' plugin 'slather' pre_install:在下載後和在安裝 Pod 前進行更改。 ruby pre_install do |installer|   # Do something fancy! end *pre_integrate:在 project 寫入磁碟前進行更改。 ruby pre_integrate do |installer|   # perform some changes on dependencies end post_install:對生成 project 寫入磁碟前進行最後的修改。 ruby post_install do |installer|   installer.pods_project.targets.each do |target|     target.build_configurations.each do |config|       config.build_settings['GCC_ENABLE_OBJC_GC'] = 'supported'     end   end end post_integrate*:在 project 寫入磁碟後進行最後更改。 ruby post_integrate do |installer|   # some change after project write to disk end

podspec 語法規範

podspec = pod Specification,意為 pod 規範,它是一個 Ruby 檔案。包含了 Pod 的庫版本詳細資訊,例如應從何處獲取源、使用哪些檔案、要應用構建設定等資訊;也可以看作該檔案是整個倉庫的索引檔案,瞭解它對我們知道 Pod 庫是如何組織、運作的提供了很大幫助。podspec 的 DSL 提供了極大的靈活性,檔案可通過 pod spec create 建立。

Root

| 名稱                         | 用途                                       | 必需     | | ---------------------------- | ------------------------------------------ | --------| | name                   | pod 名稱                                   | required | | version                | pod 版本,遵循語義化版本控制               | required | | swift_version          | 支援的 Swift 版本                          |          | | cocoapods_version      | 支援的 CocoaPods 版本                      |          | | authors                | pod 維護者的姓名和電子郵件,用“, ”進行分割 | required | | license                | pod 的許可證                               | required | | homepage               | pod 主頁的 URL                             | required | | source                 | 源地址,即原始檔的存放地址,支援多種形式源 | required | | summary                | pod 的簡短描述                             | required | | prepare_command        | 下載 pod 後執行的 bash 指令碼                |          | | static_framework       | 是否採用靜態 framework 分發                |          | | deprecated             | 該庫是否已被棄用                           |          | | deprecated_in_favor_of | 該庫名稱已被棄用,取而代之                 |          |

```ruby Pod::Spec.new do |s|   s.name             = 'CustomPod'   s.version          = '0.1.0'   s.summary          = 'A short description of CustomPod.'   s.swift_versions   = ['3.0', '4.0', '4.2']   s.cocoapods_version  =  '>= 0.36'   s.author           = { 'nihao' => '[email protected]' }   s.license          = { :type => 'MIT', :file => 'LICENSE' }   s.homepage         = 'https://github.com/XXX/CustomPod'

Supported Key

:git=> :tag, :branch, :commit,:submodules

:svn=> :folder, :tag,:revision

:hg=>:revision

:http=> :flatten, :type, :sha256, :sha1,:headers

s.source           = { :git => 'https://github.com/XX/CustomPod.git', :tag => s.version.to_s }   s.prepare_command  =  'ruby build_files.rb'   s.static_framework = true   s.deprecated       = true   s.deprecated_in_favor_of  =  'NewMoreAwesomePod' end ```

Platform

platform:pod 支援的平臺,留空意味著 pod 支援所有平臺。當支援多平臺時應該用 deployment_target 代替。

ruby spec.platform = :osx, '10.8' deployment_target:允許指定支援此 pod 的多個平臺,為每個平臺指定不同的部署目標。 ruby spec.ios.deployment_target = '6.0' spec.osx.deployment_target = '10.8'

Build settings

dependency:基於其他 pods 或子規範的依賴

ruby spec.dependency 'AFNetworking', '~> 1.0', :configurations => ['Debug']

info_plist:加入到生成的 Info.plist 的鍵值對,會對 CocoaPods 生成的預設值進行覆蓋。僅對使用 framework 的框架有影響,對靜態庫無效。對於應用規範,這些值將合併到應用程式主機的 Info.plist;對於測試規範,這些值將合併到測試包的 Info.plist。

ruby spec.info_plist = {   'CFBundleIdentifier' => 'com.myorg.MyLib',   'MY_VAR' => 'SOME_VALUE' } requires_arc:允許指定哪些 source_files 採用 ARC,不使用 ARC 的檔案將具有 -fno-objc-arc 編譯器標誌

ruby spec.requires_arc = false spec.requires_arc = 'Classes/Arc' spec.requires_arc = ['Classes/*ARC.m', 'Classes/ARC.mm']

frameworks:使用者 target 需要連結的系統框架列表

ruby spec.ios.framework = 'CFNetwork' spec.frameworks = 'QuartzCore', 'CoreData'

weak_frameworks:使用者 target 需要弱連結的框架列表

swift spec.weak_framework = 'Twitter' spec.weak_frameworks = 'Twitter', 'SafariServices' libraries:使用者 target 需要連結的系統庫列表

ruby spec.ios.library = 'xml2' spec.libraries = 'xml2', 'z'

compiler_flags:應傳遞給編譯器的 flags

ruby spec.compiler_flags = '-DOS_OBJECT_USE_OBJC=0', '-Wno-format'

*pod_target_xcconfig*:將指定 flag 新增到最終 pod 的 xcconfig 檔案 ruby spec.pod_target_xcconfig = { 'OTHER_LDFLAGS' => '-lObjC' }

user_target_xcconfig:🙅 將指定 flag 新增到最終聚合的 target 的 xcconfig,不推薦使用此屬性,因為會汙染使用者的構建設定,可能會導致衝突。

ruby spec.user_target_xcconfig = { 'MY_SUBSPEC' => 'YES' }

prefix_header_contents:🙅 在 Pod 中注入的預編譯內容,不推薦使用此屬性,因為其會汙染使用者或者其他庫的預編譯頭。

ruby spec.prefix_header_contents = '#import <UIKit/UIKit.h>', '#import <Foundation/Foundation.h>'

prefix_header_file:預編譯標頭檔案,false 表示不生成預設的 CocoaPods 的與編譯標頭檔案。🙅 不推薦使用路徑形式,因為其會汙染使用者或者其他庫的預編譯頭。

ruby spec.prefix_header_file = 'iphone/include/prefix.pch' spec.prefix_header_file = false

module_name:生成的 framrwork / clang module 使用的名稱,而非預設名稱。

ruby spec.module_name = 'Three20'

header_dir:儲存標頭檔案的目錄,這樣它們就不會被破壞。

ruby spec.header_dir = 'Three20Core'

header_mappings_dir:用於保留標頭檔案資料夾的目錄。如未提供,標頭檔案將被碾平。

ruby spec.header_mappings_dir = 'src/include'

script_phases:該屬性允許定義指令碼在 pod 編譯時執行,其作為 xcode build 命令的一部分執行,還可以利用編譯期間所設定的環境變數。

ruby spec.script_phases = [     { :name => 'Hello World', :script => 'echo "Hello World"' },     { :name => 'Hello Ruby World', :script => 'puts "Hello World"', :shell_path => '/usr/bin/ruby' },   ]

File patterns

檔案模式指定了庫的所有檔案管理方式,如原始碼、標頭檔案、framework、libaries、以及各種資源。其檔案模式萬用字元形式可參考 LINK

source_files:指定原始檔

ruby spec.source_files = 'Classes/**/*.{h,m}', 'More_Classes/**/*.{h,m}'

public_header_files:指定公共標頭檔案,這些標頭檔案與原始檔匹配,並生成文件向用戶提供。如果未指定,則將 source_files 中的所有標頭檔案都包含生成。

ruby spec.public_header_files = 'Headers/Public/*.h'

project_header_files:指定專案標頭檔案,與公共標頭檔案相對應,以排除不應向用戶專案公開且不應用於生成文件的標頭,且不會出現在構建目錄中。

ruby spec.project_header_files = 'Headers/Project/*.h'

private_header_files:私有標頭檔案,與公共標頭檔案對應,以排除不應向用戶專案公開且不應用於生成文件的標頭,這些標頭檔案會出現在產物中的 PrivateHeader 資料夾中。

ruby spec.private_header_files = 'Headers/Private/*.h'

vendered_frameworks:pod 附加的 framework 路徑

ruby spec.ios.vendored_frameworks = 'Frameworks/MyFramework.framework' spec.vendored_frameworks = 'MyFramework.framework', 'TheirFramework.xcframework'

vendered_libraries:pod 附加的 libraries 路徑

ruby spec.ios.vendored_library = 'Libraries/libProj4.a' spec.vendored_libraries = 'libProj4.a', 'libJavaScriptCore.a'

on_demand_resources:根據 Introducing On demand Resources 按需載入資源,不推薦與主工程共享標籤,預設類別為 category => :download_on_demand

ruby s.on_demand_resources = {   'Tag1' => { :paths => ['file1.png', 'file2.png'], :category => :download_on_demand } } s.on_demand_resources = {   'Tag1' => { :paths => ['file1.png', 'file2.png'], :category => :initial_install } }

resources:為 pod 構建的 bundle 的名稱和資原始檔,其中 key 為 bundle 名稱,值代表它們應用的檔案模式。

ruby spec.resource_bundles = { 'MapBox' => ['MapView/Map/Resources/*.png'],     'MapBoxOtherResources' => ['MapView/Map/OtherResources/*.png'] }

exclude_files:排除的檔案模式列表

ruby spec.ios.exclude_files = 'Classes/osx' spec.exclude_files = 'Classes/**/unused.{h,m}'

preserve_paths:下載後不應刪除的檔案。預設情況下,CocoaPods 會刪除與其他檔案模式不匹配的所有檔案

ruby spec.preserve_path = 'IMPORTANT.txt' spec.preserve_paths = 'Frameworks/*.framework'

module_map:pod 繼承為 framework 時使用的模組對映檔案,預設為 true,CocoaPods 根據 公共標頭檔案建立 module_map 檔案。

ruby spec.module_map = 'source/module.modulemap' spec.module_map = false

Subspecs

subspec:子模組的規範;實行雙重繼承:specs 自動繼承所有 subspec 作為依賴項(除非指定預設 spec);subspec 繼承了父級的屬性;

```ruby

採用不同原始檔的 Specs, CocoaPods 自動處理重複引用問題

subspec 'Twitter' do |sp|   sp.source_files = 'Classes/Twitter' end

subspec 'Pinboard' do |sp|   sp.source_files = 'Classes/Pinboard' end

引用其他子規範

s.subspec "Core" do |ss|     ss.source_files  = "Sources/Moya/", "Sources/Moya/Plugins/"     ss.dependency "Alamofire", "~> 5.0"     ss.framework  = "Foundation"   end   s.subspec "ReactiveSwift" do |ss|     ss.source_files = "Sources/ReactiveMoya/"     ss.dependency "Moya/Core"     ss.dependency "ReactiveSwift", "~> 6.0"   end   s.subspec "RxSwift" do |ss|     ss.source_files = "Sources/RxMoya/"     ss.dependency "Moya/Core"     ss.dependency "RxSwift", "~> 5.0"   end end

巢狀子規範

Pod::Spec.new do |s|   s.name = 'Root'   s.subspec 'Level_1' do |sp|     sp.subspec 'Level_2' do |ssp|     end   end end ```

default_subspecs:預設子規範陣列名稱,不指定將全部子規範作為預設子規範,:none 表示不需要任何子規範。

ruby spec.default_subspec = 'Core' spec.default_subspecs = 'Core', 'UI' spec.default_subspecs = :none

scheme:用以給指定 scheme configuration 新增拓展

ruby spec.scheme = { :launch_arguments => ['Arg1'] } spec.scheme = { :launch_arguments => ['Arg1', 'Arg2'], :environment_variables => { 'Key1' => 'Val1'} }

test_spec:測試規範,在 1.8 版本支援。可參考:CocoaPods 1.8 Beta

requires_app_host:是否需要宿主 APP 執行測試,僅適用於測試規範。

app_host_name:必要時作用於應用程式的應用程式規範名稱

app_spec:宿主 APP 規範

ruby Pod::Spec.new do |s|   s.name         = 'CannonPodder'   s.version      = '1.0.0'   # ...rest of attributes here   s.app_spec 'DemoApp' do |app_spec|     app_spec.source_files = 'DemoApp/**/*.swift'     # Dependency used only by this app spec.     app_spec.dependency 'Alamofire'   end   s.test_spec 'Tests' do |test_spec|     test_spec.requires_app_host = true     # Use 'DemoApp' as the app host.     test_spec.app_host_name = 'CannonPodder/DemoApp'     # ...rest of attributes here     # This is required since 'DemoApp' is specified as the app host.     test_spec.dependency 'CannonPodder/DemoApp'   end end

Multi-Platform support

儲存特定於某一個平臺的值,分別為 ios、osx、macOS、tvos、watchos:

ruby spec.resources = 'Resources/**/*.png' spec.ios.resources = 'Resources_ios/**/*.png'

Pod 的開發流程

瞭解完 Podfile 和 podspec 的相關的規範之後,那麼開發自己的 pod 應該是一件駕輕就熟的事。

Spec Repo

Spec Repo 是 podspec 的倉庫,即是儲存相關的 podspec 檔案的地方。本地源儲存於 ~/.cocoapods/repos中,它從 git 上拉取並完全保留目錄結構。可以發現, Master Specs Repo 的現在目錄結構有些特殊;以往版本的 Master Spec Repo 是完全在同一目錄下的,但若大量檔案在同一目錄中會導致了 Github 下載慢 的問題。為解決這個問題,採用散列表形式處理。具體方式為對名稱進行 MD5 計算得到雜湊值,取前三位作為目錄字首,以對檔案分散化。初次之外,CocoaPods 後續還採用 CDN 以及 trunk 進一步加快下載速度,有興趣可以參考 CocoaPods Source 管理機制

如:md5("CJFoundation") => 044d913fdd5a52b303222c357521f744CJFoundation 則在 /Specs/0/4/4 目錄中

Screenshot 2022-12-15 at 17.15.49.png

Create

只需利用  pod lib create [PodName] 命令便可以快速建立一個自己的 pod 。填寫好使用平臺、使用語言、是否包含 Demo、測試框架等資訊,CocoaPods 會從預設的 Git 地址中拉取一份 pod 模版,同時也可以通過 --template-url=URL 指定模版地址。在執行完後,整個檔案結構如下:

swift tree CustomPod -L 2 CustomPod ├── CustomPod │   ├── Assets // 存放資原始檔 │   └── Classes │       └── RemoveMe.[swift/m] // 單一檔案以確保最初編譯工作 ├── CustomPod.podspec // Pod 的 spec 檔案, 是一個 Pod 依賴的索引以及規範資訊 ├── Example // 用作演示/測試的示例專案 │   ├── CustomPod │   ├── CustomPod.xcodeproj │   ├── CustomPod.xcworkspace │   ├── Podfile │   ├── Podfile.lock │   ├── Pods │   └── Tests ├── _Pods.xcodeproj -> Example/Pods/Pods.xcodeproj // 指向 Pods 專案的以獲得 Carthage 支援 ├── LICENSE // 許可證 └── README.md  // 自述檔案

Development

將原始檔和資源分別放入 Classes / Assets 資料夾中,或者按你喜歡的方式組織檔案,並在 podspec 檔案中編輯相應項。如果你有任何想使用的配置項,可參考前面的podsepc 語法規範 。

一般來說,開發 Pod 一般都是作為本地 Pod 被其他 Project 所依賴進行開發,無論是使用 example 資料夾的 project 或者其他的 Project。

pod 'Name', :path => '~/CustomPod/'

Testing

通過 pod lib lint 以驗證 Pod 倉庫的使用是否正常。

Release

前面提到過 podspec 可以看作是整個倉庫的索引檔案,有了這個檔案也就能組織起一個 Pod。因此官方的源以及私有源都只需要 podspec 即可,而其他檔案則應推送到 podspec 中 source 中指定倉庫,這個倉庫應該是你自建立的。

在準備釋出推送原始碼時,需要更新版本號以及在 git 上打上 tag,這是為了進行版本號匹配,因為預設情況下的 podspec 檔案中:

ruby s.source = { :git => 'https://github.com/XXX/CustomPod.git', :tag => s.version.to_s } 可能你的工作流操作如下:

```shell $ cd ~/code/Pods/NAME $ edit NAME.podspec

set the new version to 0.0.1

set the new tag to 0.0.1

$ pod lib lint $ git add -A && git commit -m "Release 0.0.1." $ git tag '0.0.1' $ git push --tags ```

存有幾種方式推送 podspec 檔案:

  1. 推送到公共倉庫,需要用到的 trunk 子命令,更多可以參考 Getting setup with Trunk

```shell

通過電子郵箱進行註冊

pod trunk register or[email protected] 'Orta Therox' --description='macbook air' 

將指定podspec檔案推送到公共倉庫中

pod trunk push [NAME.podspec] 

新增其他人作為協作者

pod trunk add-owner ARAnalytics [email protected]  ``` 2. 推送到私有源,例如 Artsy/Specs,需要用到 repo 子命令,更多可以參考 Private Pods

```shell

將私有源地址新增到本地

pod repo add REPO_NAME SOURCE_URL 

檢查私有源是否安裝成功並準備就緒

cd ~/.cocoapods/repos/REPO_NAME pod repo lint .

將Pod的podspec新增到指定REPO_NAME中

pod repo push REPO_NAME SPEC_NAME.podspec ```

  1. 不推送到任何源中,若能存在以 URL 方式檢索到 podspec檔案,則可用該 URL,一般採用倉庫地址,例如:

ruby pod 'AFNetworking', :git => 'https://github.com/XXX/CustomPod.git'

Semantic Versioning

語義化版本控制顧名思義是一種語義上的版本控制,它不要求強制遵循,只是希望開發者能夠儘量遵守。如果庫之間依賴關係過高,可能面臨版本控制被鎖死的風險(可能需要對每一個依賴庫改版才能完成某次升級);如果庫之間依賴關係過於鬆散,又將無法避免版本的混亂(可能庫相容性不再能支援以往版本),語義化版本控制正是作為這個問題的解決方案之一。無論在 CocoaPods 中,還是 Swift Packager Manager 上,官方都希望庫開發者的的版本號能遵循這一原則:

例如,給定版本號 MAJOR.MINOR.PATCH

  1. MAJOR:進行不相容的 API 更改時進行修改
  2. MINOR:向後相容的方式新增新功能時進行修改
  3. PATCH:進行向後相容的錯誤修復時進行修改

先行版本號以及版本編譯資訊可以新增到 MAJOR.MINOR.PATCH 後面以作為延伸。

CocoaPods 原理淺析

CococaPods 核心元件

CocoaPods 被 Ruby 管理,其核心部分也被分為一個一個元件。下載原始碼,可以看到 Gemfile 檔案如下,其依賴了若干個 gem,有意思的是 cp_gem 函式,通過 SKIP_UNRELEASED_VERSIONSpath 來控制是否採用本地的 gem 路徑,實現了 DEVELOPMENT 與 RELEASE 環境的切換。

```ruby SKIP_UNRELEASED_VERSIONS = false

Declares a dependency to the git repo of CocoaPods gem. This declaration is

compatible with the local git repos feature of Bundler.

def cp_gem(name, repo_name, branch = 'master', path: false)   return gem name if SKIP_UNRELEASED_VERSIONS   opts = if path            { :path => "../#{repo_name}" }          else            url = "https://github.com/CocoaPods/#{repo_name}.git"            { :git => url, :branch => branch }          end   gem name, opts end

source 'https://rubygems.org'

gemspec

group :development do   cp_gem 'claide',                'CLAide'   cp_gem 'cocoapods-core',        'Core'   cp_gem 'cocoapods-deintegrate', 'cocoapods-deintegrate'   cp_gem 'cocoapods-downloader',  'cocoapods-downloader'   cp_gem 'cocoapods-plugins',     'cocoapods-plugins'   cp_gem 'cocoapods-search',      'cocoapods-search'   cp_gem 'cocoapods-trunk',       'cocoapods-trunk'   cp_gem 'cocoapods-try',         'cocoapods-try'   cp_gem 'molinillo',             'Molinillo'   cp_gem 'nanaimo',               'Nanaimo'   cp_gem 'xcodeproj',             'Xcodeproj'   gem 'cocoapods-dependencies', '~> 1.0.beta.1'   ... end ```

這些元件相對獨立,被分成一個一個 Gem 包,在 Core Components 中,可以找到對這些元件的簡要描述。同時也可以到 CocoaPods 的 Github 中去看詳細文件。

  • CocoaPods:命令列支援與安裝程式,也會處理 CocoaPods 的所有使用者互動。
  • cocoapods-core:對模版檔案的解析,如 Podfile、.podspec 等檔案。
  • CLAide:一個簡單的命令解析器,它提供了一個快速建立功能齊全的命令列介面的 API。
  • cocoapods-downloader:用於下載原始碼,為各種型別的原始碼控制器(HTTP/SVN/Git/Mercurial) 提供下載器。它提供 tags、commites、revisions、branches 以及 zips 檔案的下載與解壓縮操作。
  • Monlinillo:CocoaPods:對於依賴仲裁演算法的封裝,它是一個具有前項檢察的回溯演算法。不僅在 pods 中,Bundler 和 RubyGems 也是使用這一套仲裁演算法。
  • Xcodeproj:通過 Ruby 來對 Xcode projects 進行創建於修改。如:指令碼管理、libraries 構建、Xcode workspece 和配置檔案的管理。
  • cocoapods-plugins:外掛管理,其中有 pod plugins 命令幫助你獲取的可用外掛列表以及開發一個新外掛等功能,具體可用 pod plugins --help 瞭解。

pod install 做了什麼

執行 pod install --verbose,會顯示 pod install 過程中的更多 debugging 資訊。下文主要參考:整體把握 CocoaPods 核心元件

經過訊息轉發與 CLAide 命令解析,最終呼叫了 CocoaPods/lib/cocoapods/installer.rb 的 install! 函式,主要流程圖如下:

ruby def install! prepare resolve_dependencies download_dependencies validate_targets clean_sandbox if installation_options.skip_pods_project_generation? show_skip_pods_project_generation_message run_podfile_post_install_hooks else integrate end write_lockfiles perform_post_install_actions end

1. Install 環境準備(prepare)

ruby def prepare   # 如果檢測出當前目錄是 Pods,直接 raise 終止   if Dir.pwd.start_with?(sandbox.root.to_path)     message = 'Command should be run from a directory outside Pods directory.'     message << "\n\n\tCurrent directory is #{UI.path(Pathname.pwd)}\n"     raise Informative, message   end   UI.message 'Preparing' do     # 如果 lock 檔案的 CocoaPods 主版本和當前版本不同,將以新版本的配置對 xcodeproj 工程檔案進行更新     deintegrate_if_different_major_version     # 對 sandbox(Pods) 目錄建立子目錄結構     sandbox.prepare     # 檢測 PluginManager 是否有 pre-install 的 plugin     ensure_plugins_are_installed!     # 執行外掛中 pre-install 的所有 hooks 方法     run_plugins_pre_install_hooks   end end

在 prepare 階段會完成 pod install 的環境準備,包括目錄結構、版本一致性以及 pre_install 的 hook。

2. 解決依賴衝突(resolve dependencies)

ruby def resolve_dependencies     # 獲取 Sources     plugin_sources = run_source_provider_hooks     # 建立一個 Analyzer     analyzer = create_analyzer(plugin_sources)     # 如果帶有 repo_update 標記     UI.section 'Updating local specs repositories' do         # 執行 Analyzer 的更新 Repo 操作         analyzer.update_repositories     end if repo_update?     UI.section 'Analyzing dependencies' do         # 從 analyzer 取出最新的分析結果,@analysis_result,@aggregate_targets,@pod_targets         analyze(analyzer)         # 拼寫錯誤降級識別,白名單過濾         validate_build_configurations     end     # 如果 deployment? 為 true,會驗證 podfile & lockfile 是否需要更新     UI.section 'Verifying no changes' do         verify_no_podfile_changes!         verify_no_lockfile_changes!     end if deployment?     analyzer end

通過 Podfile、Podfile.lock 以及 manifest.lock 等生成 Analyzer 物件,其內部會使用個 Molinillo 演算法解析得到一張依賴關係表,進行一系列的分析與依賴衝突解決。

3. 下載依賴檔案(download dependencies)

ruby def download_dependencies   UI.section 'Downloading dependencies' do     # 構造 Pod Source Installer     install_pod_sources     # 執行 podfile 定義的 pre install 的 hooks     run_podfile_pre_install_hooks     # 根據配置清理 pod sources 資訊,主要是清理無用 platform 相關內容     clean_pod_sources   end end 經過前面分析與解決依賴衝突後,這是會進行依賴下載。會根據依賴資訊是否被新新增或者修改等資訊進行下載,同時下載後也會在本地留有一份快取,其目錄在 ~/Library/Caches/CocoaPods 。

4. 驗證 targets(validate targets)

```ruby def validate_targets     validator = Xcode::TargetValidator.new(aggregate_targets, pod_targets, installation_options)     validator.validate! end

def validate!     verify_no_duplicate_framework_and_library_names     verify_no_static_framework_transitive_dependencies     verify_swift_pods_swift_version     verify_swift_pods_have_module_dependencies     verify_no_multiple_project_names if installation_options.generate_multiple_pod_projects? end ```

  • verify_no_duplicate_framework_and_library_names:驗證是否有重名的 framework / library
  • verify_no_static_framework_transitive_dependencies:驗證動態庫是否有靜態連結庫依賴。個人認為,這個驗證是不必要的,起碼不必要 error。
  • verify_swift_pods_swift_version:驗證 Swift pod 的 Swift 版本配置且相互相容
  • verify_swift_pods_have_module_dependencies:驗證 Swift pod 是否支援 module
  • verify_no_multiple_project_names:驗證沒有重名的 project 名稱

5. 生成工程(Integrate)

```ruby def integrate     generate_pods_project     if installation_options.integrate_targets?         # 整合使用者配置,讀取依賴項,使用 xcconfig 來配置         integrate_user_project     else         UI.section 'Skipping User Project Integration'     end end

def generate_pods_project     # 建立 stage sanbox 用於儲存安裝前的沙盒狀態,以支援增量編譯的對比     stage_sandbox(sandbox, pod_targets)     # 檢查是否支援增量編譯,如果支援將返回 cache result     cache_analysis_result = analyze_project_cache     # 需要重新生成的 target     pod_targets_to_generate = cache_analysis_result.pod_targets_to_generate     # 需要重新生成的 aggregate target     aggregate_targets_to_generate = cache_analysis_result.aggregate_targets_to_generate     # 清理需要重新生成 target 的 header 和 pod folders     clean_sandbox(pod_targets_to_generate)     # 生成 Pod Project,組裝 sandbox 中所有 Pod 的 path、build setting、原始檔引用、靜態庫檔案、資原始檔等     create_and_save_projects(pod_targets_to_generate, aggregate_targets_to_generate,                                 cache_analysis_result.build_configurations, cache_analysis_result.project_object_version)     # SandboxDirCleaner 用於清理增量 pod 安裝中的無用 headers、target support files 目錄     SandboxDirCleaner.new(sandbox, pod_targets, aggregate_targets).clean!     # 更新安裝後的 cache 結果到目錄 Pods/.project_cache 下     update_project_cache(cache_analysis_result, target_installation_results) end ```

將之前版本仲裁的所有元件通過 project 檔案的形式組織起來,並對 project 中做一些使用者指定的配置。

6. 寫入依賴(write lockfiles)

ruby def write_lockfiles   @lockfile = generate_lockfile   UI.message "- Writing Lockfile in #{UI.path config.lockfile_path}" do     # No need to invoke Sandbox#update_changed_file here since this logic already handles checking if the     # contents of the file are the same.     @lockfile.write_to_disk(config.lockfile_path)   end   UI.message "- Writing Manifest in #{UI.path sandbox.manifest_path}" do     # No need to invoke Sandbox#update_changed_file here since this logic already handles checking if the     # contents of the file are the same.     @lockfile.write_to_disk(sandbox.manifest_path)   end end

將依賴更新寫入 Podfile.lock 與 Manifest.lock

7. 結束回撥(perform post install action)

ruby def perform_post_install_actions   # 呼叫 HooksManager 執行每個外掛的 post_install 方法    run_plugins_post_install_hooks   # 列印過期 pod target 警告   warn_for_deprecations   # 如果 pod 配置了 script phases 指令碼,會主動輸出一條提示訊息   warn_for_installed_script_phases   # 警告移除的 master specs repo 的 specs   warn_for_removing_git_master_specs_repo   # 輸出結束資訊 `Pod installation complete!`   print_post_install_message end

最後的收尾工作,進行 post install action 的 hook 執行以及一些 warning 列印。

CocoaPods + Plugins

早在 2013 年,CocoaPods 就添加了對外掛的支援,以新增不符合依賴管理和生態系統增長為主要目標的功能。CocoaPods Plugins 可以:在 install 前後新增 hook、新增新命令到 pod、以及利用 Ruby 動態性做任何事。下面介紹一下常見的外掛:

ps:許多外掛可能許久未維護,讀者使用需自行斟酌。

不太常見概念

CocoaPods 的配置內容幾乎包含了 Xcode Build 的方方面面,因此存在許多不太常見的概念,在此做一個連結聚合以供參考。

  • Clang Module / module_map / umbrella header:Clang Module 是 Clang 16.0.0 中引入的概念,用以解決 #include / #import 標頭檔案引入導致的相關問題;module_map 是用以描述 clang module 與 header 的關係;umbrella header 則是 module_map 中的語法規範,表示指定目錄中的標頭檔案都應包含在模組中。

Modules

Clang Module

LLVM 中的 Module

  • Hmap / Xcode Header / CocoaPods Headers

Header Map 是一組標頭檔案資訊對映表,用 .hmap 字尾表示,整體結構以 Key-Value 形式儲存;Key為標頭檔案名稱、Value 為 標頭檔案實體地址。

Xcode Phases - Header 在構建配置中分為 public、private 與 project ,用以與 target 關聯;其中 public 、private 就複製到最終產物的 header 和 PrivateHeaders 中,而 project 標頭檔案不對外使用,則不會放到最終產物。

一款可以讓大型iOS工程編譯速度提升50%的工具

What are build phases?

  • Xcconfig:

一種配置檔案,用以對構建設定進行宣告與管理,比如區分不同的開發環境等。

Xcode Build Configuration Files

  • On demand resource:WWDC 2015 引入的概念,對資原始檔的按需載入。

Introducing On Demand Resources

🔗:

[1] Cocoapods.org

[2] Xcode Workspace with multiple projects

[3] 深入理解 CocoaPods

[4] 系統理解 iOS 庫與框架

[5] Cocoapods script phases

[6] CocoaPods Podfile 解析原理

[7] Semantic Versioning 2.0.0

[8] 一款可以讓大型iOS工程編譯速度提升50%的工具

[9] CocoaPods Source 管理機制

[10] 版本管理工具及 Ruby 工具鏈環境

[11] 整體把握 CocoaPods 核心元件

[12] 工程效率優化:CocoaPods優化