CocoaPods Podspec 解析原理

在 CocoaPods 中,podspec 文件主要用于描述一个 pod 库的基本信息,包括:名称、版本、源、依赖等等。本文,我们来介绍一下 CocoaPods-Core 中另一个重要的部分——podspec。

Podspec 初始化

当执行 pod install 时,CocoaPods 会从本地的 pod repo 中查找与指定的 pod 命名和版本所对应的 podspec 文件。如果没有找到,那么 CocoaPods 会更新 pod repo,这背后有着一套复杂的管理机制,关于 podspec 管理机制下一篇文章我们将进行介绍。如果找到了,那么 CocoaPods 会对 podspec 文件进行初始化。

Source 类提供了一个 specification(name, version) 方法支持加载指定命名和版本的 podspec,如下所示:

1
2
3
4
# source.rb
def specification(name, version)
Specification.from_file(specification_path(name, version))
end

CocoaPods-Core 使用 Specification/Spec 类表示 podspec,其提供了一个类方法 self.from_file 支持 podspec 初始化,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# specification.rb
def self.from_file(path, subspec_name = nil)
path = Pathname.new(path)
# 如果指定的 podspec 文件不存在,则抛出异常
unless path.exist?
raise Informative, "No podspec exists at path `#{path}`."
end

# 读取 podspec 文件内容
string = File.open(path, 'r:utf-8', &:read)
# Work around for Rubinius incomplete encoding in 1.9 mode
# 对 podspec 文本进行 UTF-8 编码
if string.respond_to?(:encoding) && string.encoding.name != 'UTF-8'
string.encode!('UTF-8')
end

# 初始化
from_string(string, path, subspec_name)
end

self.from_file 会对文件路径进行校验,并读取 podspec 文件内容,进行 UTF-8 编码后,调用 self.from_string 方法进行初始化。self.from_string 方法实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# specification.rb
def self.from_string(spec_contents, path, subspec_name = nil)
path = Pathname.new(path).expand_path
spec = nil
# 根据 podspec 的文件类型,选择不同的方式进行初始化
case path.extname
when '.podspec'
Dir.chdir(path.parent.directory? ? path.parent : Dir.pwd) do
# 执行 podspec 文件所定义的 ruby 代码
spec = ::Pod._eval_podspec(spec_contents, path)
unless spec.is_a?(Specification)
raise Informative, "Invalid podspec file at path `#{path}`."
end
end
when '.json'
spec = Specification.from_json(spec_contents)
else
raise Informative, "Unsupported specification format `#{path.extname}` for spec at `#{path}`."
end

spec.defined_in_file = path
spec.subspec_by_name(subspec_name, true)
end

self.from_string 方法的实现可以看出,podspec 支持两种文件类型,分别是:.podspec.json

对于 .podspec 文件类型,podspec 文件中定义的 ruby 代码,因此直接调用 ::Pod._eval_podspec 方法执行 ruby 代码,从而完成初始化。

对于 .json 文件类型,specification 类通过混入(mixin)实现了 from_json 方法的 JSONSupport 模块,从而完成初始化。from_json 的内部实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# specification/json.rb
def self.from_json(json)
require 'json'
# 将 json 转换为 hash
hash = JSON.parse(json)
from_hash(hash)
end

def self.from_hash(hash, parent = nil, test_specification: false, app_specification: false)
attributes_hash = hash.dup
spec = Spec.new(parent, nil, test_specification, :app_specification => app_specification)
subspecs = attributes_hash.delete('subspecs')
testspecs = attributes_hash.delete('testspecs')
appspecs = attributes_hash.delete('appspecs')

## backwards compatibility with 1.3.0
spec.test_specification = !attributes_hash['test_type'].nil?

# 将 podspec hash 赋值给 Specificaiton 对象的 attributes_hash 属性
spec.attributes_hash = attributes_hash
# 修改 Specification 对象的 subsepcs 属性
spec.subspecs.concat(subspecs_from_hash(spec, subspecs, false, false))
spec.subspecs.concat(subspecs_from_hash(spec, testspecs, true, false))
spec.subspecs.concat(subspecs_from_hash(spec, appspecs, false, true))

spec
end

这里,self.from_json 将 json 格式的 podspec 转换成了 hash,然后以此为参数调用 self.from_hash 方法进行 podspec 初始化。

self.from_hash 中初始化一个 Specification 对象,并对其相关属性进行设置。这里,最关键的就是 将 hash 赋值给 Specification 对象的 attribuets_hash 属性,从而使 Specification 对象拥有了 podspec 所描述的所有信息。

Specification 树构建

在上一节中,self.from_hash 方法完成了对 podspec 的初始化,需要注意其内部还调用了 self.subspecs_from_hash 方法,该方法的实现如下所示:

1
2
3
4
5
6
7
8
9
10
# specification/json.rb
def self.subspecs_from_hash(spec, subspecs, test_specification, app_specification)
return [] if subspecs.nil?
# 每一个 subspec 对应初始化一个 Specification 对象,并构建一个 Specification 树
subspecs.map do |s_hash|
Specification.from_hash(s_hash, spec,
:test_specification => test_specification,
:app_specification => app_specification)
end
end

方法内部对 Specification 的每一个 subspec 初始化一个 Specification 对象,并绑定父子关系,从而构建一棵 Specification 树。以 AFNetworking 为例,根据其 podspec 构建的 Specification 树如下所示。

Specification 核心

前面,我们介绍了通过 podspec 如何构建一棵 Specification 树,下面我们来看看 Specification 的核心结构,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# specification.rb
module Pod
class Specification
# subspec 的父节点
attr_reader :parent

# spec 的唯一 ID,name + version 的 hash
attr_reader :hash_value

# spec 的配置信息
attr_accessor :attributes_hash

# spec 包含的 subspec
attr_accessor :subspecs
end
end

类似于 PodfileSpecification 同样使用 attributes_hash 记录配置信息。此外,对于 subspec,会有一个 parent 属性指向其所属的 Specification 父节点。

当看到了 Specification 类的核心实现后,我们可能会想到上一篇文章中所提到的 TargetDefinition 类。两者都构建了一个树结构,两者之间是否存在什么关系?

在 Xcode 中,target 作为一个最小的可编译单元,它编译后的产物为链接库或 framework。在 CocoaPods 中,target 则由 Specification 进行描述,最终转换成 TargetDefinition,即 Xcode Target。

在 Xcode 中,一个 target 可以依赖其他 target 进行构建。对应,在 CocoaPods 中,一个 spec 可以依赖其他 subspec,从而描述整个构建关系。

subspec 可以单独作为依赖被引入到项目中,其包含以下特点:

  • 在未指定 default_subspec 的情况下,spec 的全部 subspec 都将作为依赖被引入项目
  • subspec 会主动继承父节点 spec 所定义的 attributes_hash
  • subspec 可以指定自己的源代码、资源文件、编译配置、依赖等
  • 同一 spec 内部的 subspec 之间可以存在依赖关系
  • 每个 subspec 在 pod push 时都需要 lint 通过

Specification DSL

与 Podfile 类似,Podspec 也定义了一系列 DSL,DSL 基本原理是:使用一个哈希表存储配置项,并提供一系列方法对应操作不同的配置项。具体过程包括以下几个步骤:

  • 使用 attributeroot_attribute 方法声明属性和配置
  • 属性和配置直接存储至类属性 Self.attributes
  • 遍历 Self.attributes,为每个属性名称定义动态 setter 方法
  • 属性和配置转发存储至实例属性 attributes_hash

如下所示,Specification::DSL::AttributeSupport 模块定义了上述流程中几个关键的属性和方法:Self.attributesroot_attributeattributestore_attribute

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# specification/dsl/attribute_support.rb
module Pod
class Specification
module DSL
# 类属性
class << self
attr_reader :attributes
end

module AttributeSupport
# 根配置
def root_attribute(name, options = {})
options[:root_only] = true
options[:multi_platform] = false
store_attribute(name, options)
end

# 普通配置
def attribute(name, options = {})
store_attribute(name, options)
end

# 存储方法
def store_attribute(name, options)
attr = Attribute.new(name, options)
@attributes ||= {}
@attributes[name] = attr
end
end
end
end
end

如下所示,Specification 类中定义了动态方法和存储转发逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# specification.rb
# 将属性存储至 attributes_hash
def store_attribute(name, value, platform_name = nil)
name = name.to_s
value = Specification.convert_keys_to_string(value) if value.is_a?(Hash)
value = value.strip_heredoc.strip if value.respond_to?(:strip_heredoc)
if platform_name
platform_name = platform_name.to_s
attributes_hash[platform_name] ||= {}
attributes_hash[platform_name][name] = value
else
attributes_hash[name] = value
end
end

# Spec 类加载时遍历 attributes 动态生成 setter 方法
DSL.attributes.values.each do |a|
define_method(a.writer_name) do |value|
store_attribute(a.name, value)
end

if a.writer_singular_form
alias_method(a.writer_singular_form, a.writer_name)
end
end

Podspec DSL 根据功能可以分为以下 6 大类,具体 DSL 定义可以参考官方文档——传送门

  • Root Specification:基本配置,用于描述名称、版本号等
  • Platform:指定平台约束
  • Build Settings:构建所需的依赖配置,如:dependencyframeworks
  • File Patterns:文件管理,如:源码文件、资源文件、内嵌 frameworks、内嵌 libraries
  • Subspecs:podspec 的子模块,一个子模块以一个 target 为单元进行构建
  • Multi-Platform Support:为不同的平台执行不同的资源文件

总结

整体而言,Podspec 的解析原理与 Podfile 差不多,都是定义一组 DSL,通过 DSL 方法将配置的属性保存在一个对象的哈希表中。与此同时,构建一个树从而建立相互之间的依赖关系,所有的配置信息都保存在一棵对象树中。

参考

  1. CocoaPods-Core
  2. PodSpec 文件分析