引子
在上文 Podfile 解析逻辑 中,我们以 Xcode 工程结构作为切入点介绍了 Podfile 背后对应的数据结构,剖析了 Podfile
文件是如何解析与加载,并最终 “入侵” 项目影响其工程结构的。今天我们来聊一聊 CocoaPods-Core 中的另一个重要文件 — PodSpec
以及它所撑起的 CocoaPods 世界。
一个 Pod
的创建和发布离不开 PodSpec
文件,它可以很简单也能复杂,如 QMUIKit(后续介绍)。
今天我们就直奔主题,来分析 PodSpec
文件。
Tips:如果未看过前文,强烈推荐。
PodSpec
The Specification provides a DSL to describe a Pod. A pod is defined as a library originating from a source. A specification can support detailed attributes for modules of code through subspecs.
PodSpec
是用于描述一个 Pod 库的源代码和资源将如何被打包编译成链接库或 framework 的文件,而 PodSpec
中的这些描述内容最终将映会映射到 Specification
类中(以下简称 Spec)。
让我们来重新认识 PodSpec
。
PodSpec 初探
PodSpec
支持的文件格式为 .podspec
和 .json
两种,而 .podspec
本质是 Ruby 文件。
问题来了,为什么是 JSON 格式而不像 Podfile
一样支持 YAML 呢?
笔者的理解:由于 PodSpec
文件会满世界跑,它可能存在于 CocoaPods 的 CDN Service、Speces Repo 或者你们的私有 Specs Repo 上,因此采用 JSON 的文件在网络传输中会更友好。而 Podfile
更多的场景是用于序列化,它需要在项目中生成一份经依赖仲裁后的 Podfile
快照,用于后续的对比。
PodSpec
1Pod::Spec.new do |spec|
2 spec.name = 'Reachability'
3 spec.version = '3.1.0'
4 spec.license = { :type => 'BSD' }
5 spec.homepage = 'https://github.com/tonymillion/Reachability'
6 spec.authors = { 'Tony Million' => 'tonymillion@gmail.com' }
7 spec.summary = 'ARC and GCD Compatible Reachability Class for iOS and OS X.'
8 spec.source = { :git => 'https://github.com/tonymillion/Reachability.git', :tag => "v#{spec.version}" }
9 spec.source_files = 'Reachability.{h,m}'
10 spec.framework = 'SystemConfiguration'
11end
上面这份 Reachability.podspec
配置,基本通过命令行 pod lib create NAME
就能帮我们完成。除此之外我们能做的更多,比如,默认情况下 CococaPods 会为每个 pod
framework 生成一个对应的 modulemap 文件,它将包含 PodSpec
中指定的公共 headers。如果需要自定义引入的 header 文件,仅需配置 moduel_map
即可完成。
下面是进阶版配置:
1Pod::Spec.new do |spec|
2 spec.name = 'Reachability'
3 # 省略与前面相同部分的配置 ...
4
5 spec.module_name = 'Rich'
6 spec.swift_version = '4.0'
7
8 spec.ios.deployment_target = '9.0'
9 spec.osx.deployment_target = '10.10'
10
11 spec.source_files = 'Reachability/common/*.swift'
12 spec.ios.source_files = 'Reachability/ios/*.swift', 'Reachability/extensions/*.swift'
13 spec.osx.source_files = 'Reachability/osx/*.swift'
14
15 spec.framework = 'SystemConfiguration'
16 spec.ios.framework = 'UIKit'
17 spec.osx.framework = 'AppKit'
18
19 spec.dependency 'SomeOtherPod'
20end
像 👆 我们为不同的系统指定了不同的源码和依赖等,当然可配置的不只这些。
PodSpec
支持的完整配置分类如下:
想了解更多的配置选项:传送门。
Convention Over Configuration
Convention over configuration (aka:coding by convention) is a software design paradigm used by software frameworks that attempts to decrease the number of decisions that a developer using the framework is required to make without necessarily losing flexibility.
说到配置,不得不提一下 CoC
约定大于配置。约定大于配置算是在软件工程较早出现的概念的了,大意是:为了简单起见,我们的代码需要按照一定的约定来编写(如代码放在什么目录,用什么文件名,用什么类名等)。 这样既简化了配置文件,同时也降低了学习成本。
约定大于配置可以说是通过 Ruby on Rails 发扬光大的。尽管它一直饱受争议,但是主流语言的依赖管理工具,如 Maven,npm 等都遵循 CoC
进行不断演进的,因为 CoC
能够有效帮助开发者减轻选择的痛感,减少无意义的选择。一些新的语言也吸收了这个思想,比如 Go 语言。如果用 C/C++ 可能需要定义复杂的 Makefile 来定义编译的规则,以及如何运行测试用例,而在 Go 中这些都是约定好的。
举个 🌰 :Podfile
中是可以指定 pod library 所链接的 Xcode project,不过大多情况下无需配置,CocoaPods 会自动查找 Podfile
所在的同级目录下所对应的工程文件 .project
。
Spec 的核心数据结构
Specification
在数据结构上 Specification
与 TargetDefinition 是类似的,同为多叉树结构。简化后的 Spec
的类如下:
1require 'active_support/core_ext/string/strip.rb'
2# 记录对应 platform 上 Spec 的其他 pod 依赖
3require 'cocoapods-core/specification/consumer'
4# 解析 DSL
5require 'cocoapods-core/specification/dsl'
6# 校验 Spec 的正确性,并抛出对应的错误和警告
7require 'cocoapods-core/specification/linter'
8# 用于解析 DSL 内容包含的配置信息
9require 'cocoapods-core/specification/root_attribute_accessors'
10# 记录一个 Pod 所有依赖的 Spec 来源信息
11require 'cocoapods-core/specification/set'
12# json 格式数据解析
13require 'cocoapods-core/specification/json'
14
15module Pod
16 class Specification
17 include Pod::Specification::DSL
18 include Pod::Specification::DSL::Deprecations
19 include Pod::Specification::RootAttributesAccessors
20 include Pod::Specification::JSONSupport
21
22 # `subspec` 的父节点
23 attr_reader :parent
24 # `Spec` 的唯一 id,由 name + version 的 hash 构成
25 attr_reader :hash_value
26 # 记录 `Spec` 的配置信息
27 attr_accessor :attributes_hash
28 # `Spec` 包含的 `subspec`
29 attr_accessor :subspecs
30
31 # 递归调用获取 Specification 的根节点
32 def root
33 parent ? parent.root : self
34 end
35
36 def hash
37 if @hash_value.nil?
38 @hash_value = (name.hash * 53) ^ version.hash
39 end
40 @hash_value
41 end
42 # ...
43end
Specification
同样用 map attributes_hash
来记录配置信息。
⚠️ 这里的 parent 是为 subspec
保留的,用于指向其父节点的 Spec
。
Subspecs
A library can specify a dependency on either another library, a subspec of another library, or a subspec of itself.
乍一听 Subspec
这个概念似乎有一些抽象,不过当你理解了上面的描述,就能明白什么是 Subspec
了。我们知道在 Xcode 项目中,target 作为最小的可编译单元,它编译后的产物为链接库或 framework。而在 CocoaPods 的世界里这些 targets 则是由 Spec
文件来描述的,它还能拆分成一个或者多个 Subspec
,我们暂且把它称为 Spec
的子模块,子模块也是用 Specification 类来描述的。
**子模块可以单独作为依赖被引入到项目中。**它有几个特点:
- 未指定
default_subspec
的情况下,Spec
的全部子模块都将作为依赖被引入; - 子模块会主动继承其父节点
Spec
中定义的attributes_hash
; - 子模块可以指定自己的源代码、资源文件、编译配置、依赖等;
- 同一
Spec
内部的子模块是可以有依赖关系的; - 每个子模块在
pod push
的时候是需要被 lint 通过的;
光听总结似乎还是云里雾里,祭出 QMUI 让大家感受一下:
1Pod::Spec.new do |s|
2 s.name = "QMUIKit"
3 s.version = "4.2.1"
4 # ...
5 s.subspec 'QMUICore' do |ss|
6 ss.source_files = 'QMUIKit/QMUIKit.h', 'QMUIKit/QMUICore', 'QMUIKit/UIKitExtensions'
7 ss.dependency 'QMUIKit/QMUIWeakObjectContainer'
8 ss.dependency 'QMUIKit/QMUILog'
9 end
10
11 s.subspec 'QMUIWeakObjectContainer' do |ss|
12 ss.source_files = 'QMUIKit/QMUIComponents/QMUIWeakObjectContainer.{h,m}'
13 end
14
15 s.subspec 'QMUILog' do |ss|
16 ss.source_files = 'QMUIKit/QMUIComponents/QMUILog/*.{h,m}'
17 end
18
19 s.subspec 'QMUIComponents' do |ss|
20 ss.dependency 'QMUIKit/QMUICore'
21
22 ss.subspec 'QMUIButton' do |sss|
23 sss.source_files = 'QMUIKit/QMUIComponents/QMUIButton/QMUIButton.{h,m}'
24 end
25 # 此处省略 59 个 Components
26 end
27 # ...
28end
不吹不黑,QMUI 是笔者见过国内开源作品中代码注释非常详尽且提供完整 Demo 的项目之一。
QMUI 🐂 🍺
整个 QMUIKit 的 Spec
文件中,总共定义了 64 个 subspec
子模块,同时这些子模块之间还做了分层。比如 QMUICore:
另外补充一点,CocoaPods 支持了不同类型的 SubSpec
:
1# lib/cocoapods-core/specification/dsl/attribute_support.rb
2
3SUPPORTED_SPEC_TYPES = [:library, :app, :test].freeze
:app
和 :test
用于在项目中集成单元测试代码的 Subspec
。
PodSpec From JSON
有了上文 Podfile
的了解,这次我们对 PodSpec
的文件加载会更加轻车熟路。首先是由 #from_file
方法进行文件路径和内容编码格式的检查,将加载的内容转入 #from_string
:
1def self.from_file(path, subspec_name = nil)
2 path = Pathname.new(path)
3 unless path.exist?
4 raise Informative, "No podspec exists at path `#{path}`."
5 end
6
7 string = File.open(path, 'r:utf-8', &:read)
8 # Work around for Rubinius incomplete encoding in 1.9 mode
9 if string.respond_to?(:encoding) && string.encoding.name != 'UTF-8'
10 string.encode!('UTF-8')
11 end
12
13 from_string(string, path, subspec_name)
14end
15
16def self.from_string(spec_contents, path, subspec_name = nil)
17 path = Pathname.new(path).expand_path
18 spec = nil
19 case path.extname
20 when '.podspec'
21 Dir.chdir(path.parent.directory? ? path.parent : Dir.pwd) do
22 spec = ::Pod._eval_podspec(spec_contents, path)
23 unless spec.is_a?(Specification)
24 raise Informative, "Invalid podspec file at path `#{path}`."
25 end
26 end
27 when '.json'
28 spec = Specification.from_json(spec_contents)
29 else
30 raise Informative, "Unsupported specification format `#{path.extname}` for spec at `#{path}`."
31 end
32
33 spec.defined_in_file = path
34 spec.subspec_by_name(subspec_name, true)
35end
接着根据文件类型为 .podspec
和 .json
分别采用不同的解析方式。在 JSONSupport 模块内将 #from_json
的逻辑拆成了两部分:
1# `lib/cocoapods-core/specification/json.rb`
2module Pod
3 class Specification
4 module JSONSupport
5 # ①
6 def self.from_json(json)
7 require 'json'
8 hash = JSON.parse(json)
9 from_hash(hash)
10 end
11 # ②
12 def self.from_hash(hash, parent = nil, test_specification: false, app_specification: false)
13 attributes_hash = hash.dup
14 spec = Spec.new(parent, nil, test_specification, :app_specification => app_specification)
15 subspecs = attributes_hash.delete('subspecs')
16 testspecs = attributes_hash.delete('testspecs')
17 appspecs = attributes_hash.delete('appspecs')
18
19 ## backwards compatibility with 1.3.0
20 spec.test_specification = !attributes_hash['test_type'].nil?
21
22 spec.attributes_hash = attributes_hash
23 spec.subspecs.concat(subspecs_from_hash(spec, subspecs, false, false))
24 spec.subspecs.concat(subspecs_from_hash(spec, testspecs, true, false))
25 spec.subspecs.concat(subspecs_from_hash(spec, appspecs, false, true))
26
27 spec
28 end
29 # ③
30 def self.subspecs_from_hash(spec, subspecs, test_specification, app_specification)
31 return [] if subspecs.nil?
32 subspecs.map do |s_hash|
33 Specification.from_hash(s_hash, spec,
34 :test_specification => test_specification,
35 :app_specification => app_specification)
36 end
37 end
38 end
39end
这里的逻辑也是比较简单:
① 将传入的字符串转换为 json;
② 将转换后的 json 转换为 Spec
对象并将 json 转换为 attributes_hash
,同时触发 ③;
③ 通过 self.subspecs_from_hash
实现递归调用完成 subspecs
解析;
Tips: 方法 ② 里的 Spec 是对
Specification
的别名。
PodSpec From Ruby
QMUIKit.podspec
的文件内容,大家是否注意到其开头的声明:
1Pod::Spec.new do |s|
2 s.name = "QMUIKit"
3 s.source_files = 'QMUIKit/QMUIKit.h'
4 # ...
5end
发现没 .podspec
文件就是简单直接地声明了一个 Specifiction
对象,然后通过 block 块定制来完成配置。像 name
、source_files
这些配置参数最终都会转换为方法调用并将值存入 attributes_hash
中。这些方法调用的实现方式分两种:
- 大部分配置是通过方法包装器
attribute
和root_attribute
来动态添加的 setter 方法; - 对于复杂逻辑的配置则直接方法声明,如
subspec
、dependency
方法等(后续介绍)。
attribute wrappter
1# `lib/cocoapods-core/specification/dsl.rb`
2module Pod
3 class Specification
4 module DSL
5 extend Pod::Specification::DSL::AttributeSupport
6 # Deprecations must be required after include AttributeSupport
7 require 'cocoapods-core/specification/dsl/deprecations'
8
9 attribute :name,
10 :required => true,
11 :inherited => false,
12 :multi_platform => false
13
14 root_attribute :version,
15 :required => true
16 # ...
17 end
18 end
19end
可以看出 name 和 version 的方法声明与普通的不太一样,其实 attribute
和 root_attribute
是通过 Ruby 的方法包装器来实现的,感兴趣的同学看这里 「Python装饰器 与 Ruby实现」。
Tips: Ruby 原生提供的属性访问器 —
attr_accessor
大家应该不陌生,就是通过包装器实现的。
这些装饰器所声明的方法会在其模块被加载时动态生成,来看其实现:
1# `lib/cocoapods-core/specification/attribute_support.rb`
2module Pod
3 class Specification
4 module DSL
5 class << self
6 attr_reader :attributes
7 end
8
9 module AttributeSupport
10 def root_attribute(name, options = {})
11 options[:root_only] = true
12 options[:multi_platform] = false
13 store_attribute(name, options)
14 end
15
16 def attribute(name, options = {})
17 store_attribute(name, options)
18 end
19
20 def store_attribute(name, options)
21 attr = Attribute.new(name, options)
22 @attributes ||= {}
23 @attributes[name] = attr
24 end
25 end
26 end
27end
attribute
和 root_attribute
最终都走到了 store_attribute
保存在创建的 Attribute 对象内,并以配置的 Symbol 名称作为 KEY 存入 @attributes
,用于生成最终的 attributes setter 方法。
最关键的一步,让我们回到 specification 文件:
1# `/lib/coocapods-core/specification`
2module Pod
3 class Specification
4 # ...
5
6 def store_attribute(name, value, platform_name = nil)
7 name = name.to_s
8 value = Specification.convert_keys_to_string(value) if value.is_a?(Hash)
9 value = value.strip_heredoc.strip if value.respond_to?(:strip_heredoc)
10 if platform_name
11 platform_name = platform_name.to_s
12 attributes_hash[platform_name] ||= {}
13 attributes_hash[platform_name][name] = value
14 else
15 attributes_hash[name] = value
16 end
17 end
18
19 DSL.attributes.values.each do |a|
20 define_method(a.writer_name) do |value|
21 store_attribute(a.name, value)
22 end
23
24 if a.writer_singular_form
25 alias_method(a.writer_singular_form, a.writer_name)
26 end
27 end
28 end
29end
Specification
类被加载时,会先遍历 DSL
module 加载后所保存的 attributes,再通过 define_method
动态生成对应的配置方法。最终数据还是保存在 attributes_hash
中。
Attribute
Attribute 是为了记录该配置的相关信息,例如,记录 Spec
是否为根节点、Spec
类型、所支持的 platforms、资源地址通配符等。
-
以
root_attribute
包装的配置仅用于修饰Spec
根节点,比如版本号version
只能由Spec
根节点来设置,另外还有source
、static_framework
、module_name
等; -
以
attribute
包装的配置则不限是否为Spec
根结点。我们以 AFNetworking 的source_files
为例:由于在 macOS 和 watchOS 上并没有 UIKit framwork,因此它单独将 UIKit 的相关功能拆分到了AFNetworking/UIKit
中;
1Pod::Spec.new do |s|
2 # ...
3 s.subspec 'NSURLSession' do |ss|
4 # ...
5 end
6
7 s.subspec 'UIKit' do |ss|
8 ss.ios.deployment_target = '9.0'
9 ss.tvos.deployment_target = '9.0'
10 ss.dependency 'AFNetworking/NSURLSession'
11
12 ss.source_files = 'UIKit+AFNetworking'
13 end
14end
#subspec
除了 attribute 装饰器声明的 setter 方法,还有几个自定义的方法是直接通过 eval 调用的。
1def subspec(name, &block)
2 subspec = Specification.new(self, name, &block)
3 @subspecs << subspec
4 subspec
5end
6
7def test_spec(name = 'Tests', &block)
8 subspec = Specification.new(self, name, true, &block)
9 @subspecs << subspec
10 subspec
11end
12
13def app_spec(name = 'App', &block)
14 appspec = Specification.new(self, name, :app_specification => true, &block)
15 @subspecs << appspec
16 appspec
17end
这三种不同类型的 Subspec
经 eval
转换为对应的 Specification
对象,注意这里初始化后都将 parent 节点指向 self 同时存入 @subspecs
数组中,完成 SubSpec
依赖链的构造。
#dependency
对于其他 pod
依赖的添加我们通过 dependency 方法来实现:
1def dependency(*args)
2 name, *version_requirements = args
3 # dependency args 有效性校验 ...
4
5 attributes_hash['dependencies'] ||= {}
6 attributes_hash['dependencies'][name] = version_requirements
7
8 unless whitelisted_configurations.nil?
9 # configuration 白名单过滤和校验 ...
10
11 attributes_hash['configuration_pod_whitelist'] ||= {}
12 attributes_hash['configuration_pod_whitelist'][name] = whitelisted_configurations
13 end
14end
dependency 方法内部主要是对依赖有效性的校验,限于篇幅这里不列出实现,核心要点如下:
- 检查依赖循环,根据
Spec
名称判断Spec
与自身,Spec
与SubSpec
之间是否存在循环依赖; - 检查依赖来源,
PodSpec
中不支持:git
或:path
形式的来源指定,如需设定可通过Podfile
来修改; - 检查 configuation 白名单,目前仅支持 Xcode 默认的
Debug
和Release
的 configuration 配置;
创建并使用你的 Pod
最后一节来两个实践:创建 Pod 以及在项目中使用 SubSpecs
。
Pod 创建
pod 相关使用官方都提供了很详尽的都文档,本小节仅做介绍。
0x1 创建 Pod
仅需一行命令完成 Pod
创建(文档):
1$ pod lib create `NAME`
之后每一步都会输出友好提示,按照提示选择即可。在添加完 source code 和 dependency 之后,你还可以在 CocoaPods 为你提供的 Example 项目中运行和调试代码。
准备就绪后,可以通过以下命令进行校验,检查 Pod
正确性:
1$ pod lib lint `[PODSPEC_PATHS ...]`
0x2 发布 Pod
校验通过后就可以将 Pod
发布了,你可以将 PodSepc
发布到 Master Repo 上,或者发布到内部的 Spec Repo 上。
CocoaPods Master Repo
如果发布的 CocoaPods 的主仓库,那么需要通过 CocoaPods 提供的 Trunk 命令:
1$ pod trunk push `[NAME.podspec]`
不过使用前需要先通过邮箱注册,详情查看文档。
Private Spec Repo
对于发布到私有仓库的,可通过 CocoaPods 提供的 Repo 命令:
1$ pod repo push `REPO_NAME` `SPEC_NAME.podspec`
文档详情 — 传送门。
SubSpecs In Podfile
在 SubSpec
一节提到过,在 CocoaPods 中 SubSpec
是被作为单独的依赖来看待的,这里就借这个实操来证明一下。
在上文的实践中,我们知道每一个 Pod
库对应为 Xcode 项目中的一个个 target,那么当明确指定部分 SubSpec
时,它们也将被作为独立的 target 进行编译。不过这里需要明确一下使用场景:
0x1 Single Target
当主项目中仅有一个 target 或多个 target 引用了同一个 pod
库的多个不同 SubSpec
时,生成的 target 只会有一个。我们以 QMUIKit 为例,项目 Demo.project 下的 Podfile 配置如下:
1target 'Demo' do
2 pod 'QMUIKit/QMUIComponents/QMUILabel', :path => '../QMUI_iOS'
3 pod 'QMUIKit/QMUIComponents/QMUIButton', :path => '../QMUI_iOS'
4end
此时 Pods.project
下的 QMUIKit 的 target 名称为 QMUIKit。
0x2 Multiple Target
如果我们的主项目中存在多个 target 且使用同一个 pod
库的不同 SubSpec
时,结果则有所不同。
现在我们在 0x1 的基础上添加如下配置:
1target 'Demo2' do
2 pod 'QMUIKit/QMUIComponents/QMUILog', :path => '../QMUI_iOS'
3end
可以发现,CocoaPods 为每个 tareget 对应的 SubSpec
依赖生成了不同的 QMUIKit targets。
Tips: 当主工程 target 依赖的
Subspec
数量过多导致的名称超过 50 个字符,将会对 subspec 后缀做摘要处理作为唯一标识符。
总结
本文是 CocoaPods-Core 的第二篇,重点介绍了 PodSpec
的类构成和解析实现,总结如下:
- 初探
PodSpec
让我们对其能力边界和配置分类有了更好的了解; - 深入
PodSpec
我们发现其数据结构同Podfile
类似,都是根据依赖关系建立对应的树结构; PodSpec
针对单个库的源码和资源提供了更精细化的管理,SubSpec
结构的推出让大型 library 的内部分层提供了很好的工具;- 装饰器模式结合 Ruby 的动态特性,让
PodSpec
的 DSL 特性的实现起来更加优雅;
知识点问题梳理
这里罗列了四个问题用来考察你是否已经掌握了这篇文章,如果没有建议你加入收藏再次阅读:
- 说说
PodSpec
所支持的配置有几类,分别具有哪些功能 ? PodSpec
与SubSpec
之间有哪些关系 ?- 说说
SubSpec
的特点以及作用 ? - 谈谈
PodSpec
中的 DSL 解析与Podfile
的解析实现有哪些区别 ?