CocoaPods使用指南
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_PATHS
和 HEADER_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") => 044d913fdd5a52b303222c357521f744
;CJFoundation
則在 /Specs/0/4/4 目錄中
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 檔案:
- 推送到公共倉庫,需要用到的 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 ```
- 不推送到任何源中,若能存在以 URL 方式檢索到 podspec檔案,則可用該 URL,一般採用倉庫地址,例如:
ruby
pod 'AFNetworking', :git => 'https://github.com/XXX/CustomPod.git'
Semantic Versioning
語義化版本控制顧名思義是一種語義上的版本控制,它不要求強制遵循,只是希望開發者能夠儘量遵守。如果庫之間依賴關係過高,可能面臨版本控制被鎖死的風險(可能需要對每一個依賴庫改版才能完成某次升級);如果庫之間依賴關係過於鬆散,又將無法避免版本的混亂(可能庫相容性不再能支援以往版本),語義化版本控制正是作為這個問題的解決方案之一。無論在 CocoaPods 中,還是 Swift Packager Manager 上,官方都希望庫開發者的的版本號能遵循這一原則:
例如,給定版本號 MAJOR.MINOR.PATCH
:
MAJOR
:進行不相容的 API 更改時進行修改MINOR
:向後相容的方式新增新功能時進行修改PATCH
:進行向後相容的錯誤修復時進行修改
先行版本號以及版本編譯資訊可以新增到 MAJOR.MINOR.PATCH
後面以作為延伸。
CocoaPods 原理淺析
CococaPods 核心元件
CocoaPods 被 Ruby 管理,其核心部分也被分為一個一個元件。下載原始碼,可以看到 Gemfile 檔案如下,其依賴了若干個 gem,有意思的是 cp_gem
函式,通過 SKIP_UNRELEASED_VERSIONS
與 path
來控制是否採用本地的 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 動態性做任何事。下面介紹一下常見的外掛:
- cocoapods-binary:一個比較早期的二進位制外掛庫,是諸多二進位制方案的靈感來源
- cocoapods-repo-update:自動化 pod repo update
- cocoapods-integrate-flutter:將 flutter 與現有 iOS 應用程式整合
- cocoapods-uploader:上傳檔案/目錄到遠端倉庫
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 中的語法規範,表示指定目錄中的標頭檔案都應包含在模組中。
- Hmap / Xcode Header / CocoaPods Headers
Header Map 是一組標頭檔案資訊對映表,用 .hmap 字尾表示,整體結構以 Key-Value 形式儲存;Key為標頭檔案名稱、Value 為 標頭檔案實體地址。
Xcode Phases - Header 在構建配置中分為 public、private 與 project ,用以與 target 關聯;其中 public 、private 就複製到最終產物的 header 和 PrivateHeaders 中,而 project 標頭檔案不對外使用,則不會放到最終產物。
- 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 庫與框架
[10] 版本管理工具及 Ruby 工具鏈環境
[11] 整體把握 CocoaPods 核心元件
[12] 工程效率優化:CocoaPods優化