前言
对于大多数软件开发团队来说,依赖管理工具必不可少,它能针对开源和私有依赖进行安装与管理,从而提升开发效率,降低维护成本。针对不同的语言与平台,其依赖管理工具也各有不同,例如 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 进行):
sudo gem install cocoapods
如果安装成功,便可以使用 pod 的相关命令了。针对一个简单的项目来说,只需三步便可引入其他的依赖:
创建 Podfile 文件( CocoaPods 提供了 pod init 命令创建)
对 Podfile 文件进行编写,添加依赖的库,版本等信息。
在命令行执行 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 时使用的安装方法和选项。如:
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 版本不匹配时发出警告。
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 版本
# 依赖版本控制
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
仅继承父级的搜索路径。
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
过程。
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
。
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。
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
platform :ios, '4.0'
project:指定包含 target 的 Xcode project。这一般在 workspace 存在多个 xcode project 中使用:
# 在 FastGPS Project 中可以找到一个名为 MyGPSApp 的 target
target 'MyGPSApp' do
project 'FastGPS'
...
end
inhibit_all_warnings!:禁止所有警告。如果针对单个 Pod,则可以采用:
pod 'SSZipArchive', :inhibit_warnings => true
pod 'SSZipArchive', :inhibit_warnings => true
user_modular_headers!:将所有 Pod 模块化。如果针对单个 Pod,则可以采用:
pod 'SSZipArchive', :modular_headers => true
pod 'SSZipArchive', :modular_headers => false
user_frameworks!:采用 framework 而不是 .a 文件的静态库。
可以通过 :linkage 指定使用静态库还是动态库:
use_frameworks!:linkage => :dynamic / :static
supports_swift_versions:指定 target definition 支持的 swift 版本要求
supports_swift_versions '>= 3.0', '< 4.0'
Workspace
workspace:指定包含所有项目的 Xcode workspace。
workspace 'MyWorkspace'
Sources
sources:Podfile 从指定的源列表中进行检索。sources 默认存储在 ~/.cocoapods/repos 中,是全局的而非按 target definition 存储。当有多个相同的 Pod 时,优先采用检索到的 Pod 的第一个源,因此当指定另一个来源时,则需显示指定 CocoaPods 的源。
source 'https://github.com/artsy/Specs.git'
source 'https://github.com/CocoaPods/Specs.git'
Hooks
plugin:指定在安装期间使用的插件。
plugin 'cocoapods-keys', :keyring => 'Eidolon'
plugin 'slather'
pre_install:在下载后和在安装 Pod 前进行更改。
pre_install do |installer|
# Do something fancy!
end
pre_integrate:在 project 写入磁盘前进行更改。
pre_integrate do |installer|
# perform some changes on dependencies
end
post_install:对生成 project 写入磁盘前进行最后的修改。
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 写入磁盘后进行最后更改。
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 | 该库名称已被弃用,取而代之 |
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' => 'XXXX@qq.com' }
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
代替。
spec.platform = :osx, '10.8'
deployment_target:允许指定支持此 pod 的多个平台,为每个平台指定不同的部署目标。
spec.ios.deployment_target = '6.0'
spec.osx.deployment_target = '10.8'
Build settings
dependency:基于其他 pods 或子规范的依赖
spec.dependency 'AFNetworking', '~> 1.0', :configurations => ['Debug']
info_plist:加入到生成的 Info.plist 的键值对,会对 CocoaPods 生成的默认值进行覆盖。仅对使用 framework 的框架有影响,对静态库无效。对于应用规范,这些值将合并到应用程序主机的 Info.plist
;对于测试规范,这些值将合并到测试包的 Info.plist。
spec.info_plist = {
'CFBundleIdentifier' => 'com.myorg.MyLib',
'MY_VAR' => 'SOME_VALUE'
}
requires_arc:允许指定哪些 source_files 采用 ARC,不使用 ARC 的文件将具有 -fno-objc-arc
编译器标志
spec.requires_arc = false
spec.requires_arc = 'Classes/Arc'
spec.requires_arc = ['Classes/*ARC.m', 'Classes/ARC.mm']
frameworks:使用者 target 需要链接的系统框架列表
spec.ios.framework = 'CFNetwork'
spec.frameworks = 'QuartzCore', 'CoreData'
weak_frameworks:使用者 target 需要弱链接的框架列表
spec.weak_framework = 'Twitter'
spec.weak_frameworks = 'Twitter', 'SafariServices'
libraries:使用者 target 需要链接的系统库列表
spec.ios.library = 'xml2'
spec.libraries = 'xml2', 'z'
compiler_flags:应传递给编译器的 flags
spec.compiler_flags = '-DOS_OBJECT_USE_OBJC=0', '-Wno-format'
pod_target_xcconfig:将指定 flag 添加到最终 pod 的 xcconfig 文件
spec.pod_target_xcconfig = { 'OTHER_LDFLAGS' => '-lObjC' }
user_target_xcconfig:🙅 将指定 flag 添加到最终聚合的 target 的 xcconfig,不推荐使用此属性,因为会污染用户的构建设置,可能会导致冲突。
spec.user_target_xcconfig = { 'MY_SUBSPEC' => 'YES' }
prefix_header_contents:🙅 在 Pod 中注入的预编译内容,不推荐使用此属性,因为其会污染用户或者其他库的预编译头。
spec.prefix_header_contents = '#import <UIKit/UIKit.h>', '#import <Foundation/Foundation.h>'
prefix_header_file:预编译头文件,false 表示不生成默认的 CocoaPods 的与编译头文件。🙅 不推荐使用路径形式,因为其会污染用户或者其他库的预编译头。
spec.prefix_header_file = 'iphone/include/prefix.pch'
spec.prefix_header_file = false
module_name:生成的 framrwork / clang module 使用的名称,而非默认名称。
spec.module_name = 'Three20'
header_dir:存储头文件的目录,这样它们就不会被破坏。
spec.header_dir = 'Three20Core'
header_mappings_dir:用于保留头文件文件夹的目录。如未提供,头文件将被碾平。
spec.header_mappings_dir = 'src/include'
script_phases:该属性允许定义脚本在 pod 编译时执行,其作为 xcode build
命令的一部分执行,还可以利用编译期间所设置的环境变量。
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:指定源文件
spec.source_files = 'Classes/**/*.{h,m}', 'More_Classes/**/*.{h,m}'
public_header_files:指定公共头文件,这些头文件与源文件匹配,并生成文档向用户提供。如果未指定,则将 source_files 中的所有头文件都包含生成。
spec.public_header_files = 'Headers/Public/*.h'
project_header_files:指定项目头文件,与公共头文件相对应,以排除不应向用户项目公开且不应用于生成文档的标头,且不会出现在构建目录中。
spec.project_header_files = 'Headers/Project/*.h'
private_header_files:私有头文件,与公共头文件对应,以排除不应向用户项目公开且不应用于生成文档的标头,这些头文件会出现在产物中的 PrivateHeader 文件夹中。
spec.private_header_files = 'Headers/Private/*.h'
vendered_frameworks:pod 附加的 framework 路径
spec.ios.vendored_frameworks = 'Frameworks/MyFramework.framework'
spec.vendored_frameworks = 'MyFramework.framework', 'TheirFramework.xcframework'
vendered_libraries:pod 附加的 libraries 路径
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
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 名称,值代表它们应用的文件模式。
spec.resource_bundles = {
'MapBox' => ['MapView/Map/Resources/*.png'],
'MapBoxOtherResources' => ['MapView/Map/OtherResources/*.png']
}
exclude_files:排除的文件模式列表
spec.ios.exclude_files = 'Classes/osx'
spec.exclude_files = 'Classes/**/unused.{h,m}'
preserve_paths:下载后不应删除的文件。默认情况下,CocoaPods 会删除与其他文件模式不匹配的所有文件
spec.preserve_path = 'IMPORTANT.txt'
spec.preserve_paths = 'Frameworks/*.framework'
module_map:pod 继承为 framework 时使用的模块映射文件,默认为 true,CocoaPods 根据 公共头文件创建 module_map 文件。
spec.module_map = 'source/module.modulemap'
spec.module_map = false
Subspecs
subspec:子模块的规范;实行双重继承:specs 自动继承所有 subspec 作为依赖项(除非指定默认 spec);subspec 继承了父级的属性;
# 采用不同源文件的 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
表示不需要任何子规范。
spec.default_subspec = 'Core'
spec.default_subspecs = 'Core', 'UI'
spec.default_subspecs = :none
scheme:用以给指定 scheme configuration 添加拓展
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 规范
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:
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
指定模版地址。在执行完后,整个文件结构如下:
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 文件中:
s.source = { :git => 'https://github.com/XXX/CustomPod.git', :tag => s.version.to_s }
可能你的工作流操作如下:
$ 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:
# 通过电子邮箱进行注册
pod trunk register orta@cocoapods.org 'Orta Therox' --description='macbook air'
# 将指定podspec文件推送到公共仓库中
pod trunk push [NAME.podspec]
# 添加其他人作为协作者
pod trunk add-owner ARAnalytics kyle@cocoapods.org
- 推送到私有源,例如 Artsy/Specs,需要用到 repo 子命令,更多可以参考 Private Pods:
# 将私有源地址添加到本地
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,一般采用仓库地址,例如:
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 环境的切换。
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! 函数,主要流程图如下:
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)
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)
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)
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)
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)
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)
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)
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 中的语法规范,表示指定目录中的头文件都应包含在模块中。
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优化
作者:nihao1011
链接:https://juejin.cn/post/7179231344147300412
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。