CocoaPods Podfile 解析原理

作为 iOS 开发者,我们都知道 Podfile 是 CocoaPods 用于描述 Xcode 项目依赖的配置文件。当需要为项目添加依赖时,我们只需要在 Podfile 中声明一个 pod 即可,比如:

1
pod 'Alamofire', '~> 5.4'

当我们执行了 pod install 之后,项目就集成并依赖了这个第三方 pod 库了。那么,这背后的原理到底是什么呢?

为了探索 Podfile 背后的原理,我们来看 CocoaPods-Core 的相关源码实现,这里我们主要关注 3 个源码文件:

  • podfile/dsl.rb
  • podfile/target_definition.rb
  • podfile.rb

示例

下文我们以一个 Demo Project 的例子进行介绍,如下所示为 Demo Project 的 Podfile 文件定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
source 'https://cdn.cocoapods.org/'

platform :ios, '11.0'

inhibit_all_warnings!
use_modular_headers!
install! 'cocoapods', :warn_for_unused_master_specs_repo => false

target 'Demo' do
pod 'Alamofire', '~> 5.4'
pod 'Dollar'
pod 'SnapKit', '~> 4.0'

target 'DemoTests' do
inherit! :search_paths
# Pods for testing
end

target 'DemoUITests' do
inherit! :search_paths
# Pods for testing
end

end

Podfile 初始化

在上述例子中,Podfile 文件声明了项目的依赖,诸如:AlamofireDollarSnapKit 等。当我们执行 pod install 命令时,CocoaPods 会根据 Podfile 文件路径进行初始化,如下所示:

1
2
3
4
# config.rb
def podfile
@podfile ||= Podfile.from_file(podfile_path) if podfile_path
end

Podfile 类提供了类方法 self.from_file,根据 Podfile 文件路径进行初始化。self.from_file 方法的定义如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def self.from_file(path)
path = Pathname.new(path)
# 根据路径判断 Podfile 是否存在
unless path.exist?
raise Informative, "No Podfile exists at path `#{path}`."
end

# 根据路径的文件后缀名,选择对应的工厂方法进行初始化
case path.extname
when '', '.podfile', '.rb'
Podfile.from_ruby(path)
when '.yaml'
Podfile.from_yaml(path)
else
raise Informative, "Unsupported Podfile format `#{path}`."
end
end

self.from_file 的实现中,首先会根据路径判断文件是否存在,如果不存在即抛出异常。然后,判断通过文件的后缀判断文件的类型,选择不同的工厂方法进行初始化。通过溯源代码定义,我们可以发现,CocoaPods 支持的 4 种命名方式的 Podfile,分别是:

  • CocoaPods.podfile.yaml
  • CocoaPods.podfile
  • Podfile
  • Podfile.rb

对于 yaml 类型的 Podfile 文件,使用 self.from_yaml 工厂方法进行初始化;对于其他类型的 Podfile 文件,使用 self.from_ruby 工厂方法进行初始化。

这里,我们重点看一下 self.from_ruby 的实现,如下所示:

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
33
34
35
def self.from_ruby(path, contents = nil)
# 读取 Podfile 中的内容
contents ||= File.open(path, 'r:utf-8', &:read)

# 使用 UTF-8 进行编码
if contents.respond_to?(:encoding) && contents.encoding.name != 'UTF-8'
contents.encode!('UTF-8')
end

# 如果存在非法字符,则报错。
if contents.tr!('“”‘’‛', %(""'''))
# Changes have been made
CoreUI.warn "Smart quotes were detected and ignored in your #{path.basename}. " \
'To avoid issues in the future, you should not use ' \
'TextEdit for editing it. If you are not using TextEdit, ' \
'you should turn off smart quotes in your editor of choice.'
end

# 初始化 Podfile 对象,并传入尾随闭包
podfile = Podfile.new(path) do
# rubocop:disable Lint/RescueException
begin
# rubocop:disable Security/Eval
# 使用 eval 执行 Podfile 中定义的代码
eval(contents, nil, path.to_s)
# rubocop:enable Security/Eval
rescue Exception => e
message = "Invalid `#{path.basename}` file: #{e.message}"
raise DSLError.new(message, path, e, contents)
end
# rubocop:enable Lint/RescueException
end
# 返回 Podfile 对象
podfile
end

在工厂方法中,主要通过 3 个步骤来初始化 Podfile。

  • 首先,读取 Podfile 文件的内容,并进行 UTF-8 编码。
  • 然后,检测文件内容是否存在非法字符,如果存在则抛出异常。
  • 最后,通过 Podfile 类构造方法进行初始化,并传入一个尾随闭包。

注意,最后一步的尾随闭包包含了一行关键代码:eval(contents, nil, path.to_s) 。正是这行代码,执行了我们在 Podfile 文件声明的所有内容,从而使它们正式生效。我们在 Podfile 文件进行声明,本质上是编写了对一系列 ruby 方法的调用。

TargetDefinition 树构建

我们继续深入 Podfile 类的构造方法,其定义如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def initialize(defined_in_file = nil, internal_hash = {}, &block)
self.defined_in_file = defined_in_file
@internal_hash = internal_hash
if block
# ruby 类型的 Podfile 初始化逻辑
default_target_def = TargetDefinition.new('Pods', self)
default_target_def.abstract = true
@root_target_definitions = [default_target_def]
@current_target_definition = default_target_def
# 真正执行 Podfile 文件中所声明的代码
instance_eval(&block)
else
# yaml 类型的 Podfile 初始化逻辑
@root_target_definitions = []
end
end

对于 ruby 类型的 Podfile 文件,Podfile 对象初始化时,会构建一个 TargetDefinition 树的根节点。然后,通过 instance_eval 方法真正执行 Podfile 中的代码。从而在已有的 TargetDefinition 根节点的基础上,进一步构建了整棵 TargetDefinition

在 Demo Project 的例子中,Podfile 初始化后,构建的 TargetDefinition 树如下图所示:

TargetDefinition 树的层级结构与 Podfile 文件中声明的 target 嵌套层级结构相关联,如下为 Podfile 文件中 target 的定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
target 'Demo' do
...

target 'DemoTests' do
inherit! :search_paths
# Pods for testing
end

target 'DemoUITests' do
inherit! :search_paths
# Pods for testing
end

end

我们来看一下 target 方法的具体实现,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def target(name, options = nil)
if options
raise Informative, "Unsupported options `#{options}` for " \
"target `#{name}`."
end

parent = current_target_definition
# 初始化一个 TargetDefinition,并建立父子关系
definition = TargetDefinition.new(name, parent)
self.current_target_definition = definition
# 执行闭包
yield if block_given?
ensure
# 执行完毕,恢复 current_target_definition
self.current_target_definition = parent
end

target 方法中,会创建一个 TargetDefinition 对象,并将当前的 TargetDefinition 作为父对象,从而建立父子关系,构建树结构。构建完毕,执行 target 方法的尾随闭包,进行进一步的设置。最后,恢复现场。

TargetDefinition

那么 TargetDefinition 到底是什么?其实上图已经给了我们答案:一个 TargetDefinition 对应一个 Xcode Target

《理解 Xcode 中的各种概念》 一文中,我们介绍了 Target 的作用,其包含构建特定 Product 所需的 build configuration、build rule、build phase,并指定对应的构建产物 Product。默认,Target 继承 Project 定义的 build configuration,并且可以覆盖或自定义 build settings。

如下所示,为 TargetDefinition 类的主要定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module Pod
class Podfile
class TargetDefinition
# [TargetDefinition, Podfile]
attr_reader :parent

# [Array<TargetDefinition>]
attr_reader :children

protected

# [Hash]
attr_accessor :internal_hash

# Methods
# ...
end
end
end

TargetDefinition 类包含一个指向父节点的指针 parent 以及一组指向子节点的指针 children。最关键的是,TargetDefinition 通过一个内部的 internal_hash 存储了所有 TargetDefinition 的配置信息,包括:build configurations,build phases、build rules 等等。TargetDefinition 定义的其他方法几乎全部都是对 internal_hash 进行增删查改。

Hash Keys

TargetDefinition 内部通过定义一个 HASH_KEYS 的数组预定义了 internal_hash 将存储哪些键值对。HASH_KEYS 的定义如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
HASH_KEYS = %w(
name
platform
podspecs
exclusive
inhibit_warnings
use_modular_headers
user_project_path
build_configurations
project_names
dependencies
script_phases
children
configuration_pod_whitelist
uses_frameworks
swift_version_requirements
inheritance
abstract
swift_version
).freeze

对比一下 Xcode Target 的结构类图,如下所示,我们大概能找到一些对应关系,尤其是 PBXNativeTarget,如:dependenciesbuild_configurationsscript_phases 等。

DSL 解析

TargetDefinition 树构造过程中,我们用到的 target 方法定义在 Podfile DSL 中。除此之外,Podfile DSL 中还定义了一系列方法。这些 DSL 的主要作用是:为开发者提供简洁的语法,从而支持自定义配置 Target、Project、Workspace,并且提供了一系列 hook 方法,允许开发者执行自定义脚本。

Podfile DSL 本质上是一个 module,它会被 mixin 到 Podfile 类中,其内部实现与 Podfile 紧密关联。尤其是,Podfile DSL 使用了 Podfile 类中定义的 current_target_definitioninternal_hash,用于存储 Podfile 所声明的配置信息。

Podfile DSL 根据功能可以分为如下几种类型:

  • Root Options
  • Dependencies
  • Target Configuration
  • Workspace
  • Sources
  • Hooks

下面,对这些 DSL 进行简要的介绍。

Root Options

Root Options 相关的 DSL 主要用于对 Podfile 进行整体配置,如下所示:

1
2
3
4
5
6
7
8
# 声明安装过程中会使用的安装方法和选项
# 存储方式:
# internal_hash['installation_method'] => {"name" => xxx, "options" => xxx}
def install!(installation_method, options = {})

# 如果使用 Global Gemset 运行 CocoaPods,则会抛出异常。
# 如果传入 version 参数,则严格限制 bundler 的版本号。
def ensure_bundler!(version = nil)

Dependencies

Dependencies 相关的 DSL 主要用于为每个 Target 指定依赖。其主要通过调用 TargetDefinition 类所提供的方法将配置存入 TargetDefinition 类的 internal_hash 中。相关 DSL 如下所示:

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
33
34
35
36
# 创建一个特定依赖
# 存储方式:
# current_target_definition.store_pod(name, *requirements)
def pod(name = nil, *requirements)

# 创建一个 podspec
# 存储方式:
# current_target_definition.store_podspec(options)
def podspec(options = nil)

# 创建一个子 target,关联父 target,设置当前为子 target
# 存储方式:
# current_target_definition = definition
def target(name, options = nil)

# 创建一个 build phase
# 存储方式:
# current_target_definition.store_script_phase(options)
def script_phase(options)

# 将当前 target 设置成抽象 target
# 存储方式:
# current_target_definition.abstract = abstract
def abstract!(abstract = true)

# 创建一个抽象 target
def abstract_target(name)

# 设置继承模式
# 存储方式:
# current_target_definition.inheritance = inheritance
# 三种模式
# - :complete: 当前 target 继承父 target 的全部行为
# - :none: 当前 target 不继承父 target 的任何行为
# - :search_paths: 当前 target 只继承父 target 的 search paths
def inherit!(inheritance)

Target Configurations

Target Configurations 相关的 DSL 主要用于控制 CocoaPods 生成 Project 文件。类似的,其主要也通过调用 TargetDefinition 类所提供的相关方法将配置存入 TargetDefinition 类的 internal_hash 中。相关 DSL 如下所示:

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
# 设置当前 target 的目标平台
# 存储方式:
# current_target_definition.set_platform!(name, target)
def platform(name, target = nil)

# 指定 target 所在的 project。注意:Pods 库会被该 target 链接
# 同时可以为 target 指定 build configuration
# 存储方式:
# current_target_definition.user_project_path = path
# current_target_definition.build_configurations = build_configurations
def project(path, build_configurations = {})

# 设置当前 target 是否忽略所有来自 CocoaPods 库的警告
# 存储方式:
# current_target_definition.inhibit_all_warnings = true
def inhibit_all_warnings!

# 设置所有的 CocoaPods 静态库使用 modular header
# 存储方式:
# current_target_definition.use_modular_headers_for_all_pods = true
def use_modular_headers!

# 设置 Pods 使用 framework 而非静态库。注意:当使用 framework 时,我们需要指定 `:linkage` 的类型为 `:static` 或 `:dynamic`
# 存储方式:
# current_target_definition.use_frameworks!(option)
def use_frameworks!(option = true)

# 设置 target 支持的 swift 版本约束
# 存储方式:
# current_target_definition.store_swift_version_requirements(*requirements)
def supports_swift_versions(*requirements)

Workspace

Workspace 相关的 DSL 主要用于配置 Xcode Workspace,设置全局的 settings。其将配置存储在 Podfile 类的 internal_hash 中。相关 DSL 如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 设置包含所有 project 的 Xcode workspace 
# 存储方式:
# internal_hash['workspace'] => path
def workspace(path)

# 设置根据所有已安装的 Pods 的头文件生成一个 BridgeSupport 元文件
# 存储方式:
# internal_hash['generate_bridge_support'] => true
def generate_bridge_support!

# 设置为 `OTHER_LD_FLAGS` 添加 `-fobjc-arc` 标志
# 存储方式:
# internal_hash['set_arc_compatibility_flag'] => true
def set_arc_compatibility_flag!

Sources

Sources 相关的 DSL 主要用于设置源,Podfile 通过源列表查找对应的 Specification。相关的 DSL 如下所示:

1
2
3
4
# 指定 spec 的位置
# 存储方法:
# internal_hash['sources'] << source
def source(source)

Hooks

Hooks 相关的 DSL 主要用于配置自定义脚本。Podfile 提供了一系列 hooks,这些 hooks 会在 install 过程中被执行。相关的 DSL 如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 设置安装过程中会用到的插件
# internal_hash['plugins'] << name
def plugin(name, options = {})

# 在 Pods 下载完成后,安装之前,执行 hook
def pre_install(&block)

# 在 project 写入硬盘前,执行 hook
def pre_integrate(&block)

在 project 写入硬盘前,执行 hook 进行最后的修改
def post_install(&block)

# 在 project 写入硬盘后,执行 hook
def post_integrate(&block)

Podfile 对象

当 Podfile 文件中的代码执行完毕后,Podfile 对象也就构建完成了。Podfile 类的主要定义如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module Pod
class Podfile
include Pod::Podfile::DSL

class StandardError < ::StandardError; end
# podfile 路径
attr_accessor :defined_in_file
# 所有的 TargetDefinition 的根节点, 正常只有一个,即 Pods.project target
attr_accessor :root_target_definitions

private

# 记录 Pods.project 项目的配置信息
attr_accessor :internal_hash
# 当前 DSL 解析使用的 TargetDefinition
attr_accessor :current_target_definition

end
end

类似于 TargetDefinitionPodfile 也提供了一个 internal_hash 用于存储一些全局的设置,它通过一个 HASH_KEYS 数组预定义了一系列键,如下所示:

1
2
3
4
5
6
7
8
9
HASH_KEYS = %w(
installation_method
workspace
sources
plugins
set_arc_compatibility_flag
generate_bridge_support
target_definitions
).freeze

在 DSL 解析一节中,我们知道了 DSL 将配置信息分别存储在了 TargetDefinition 类的 internal_hashPodfile 类的 internal_hash 中。而 Podfile 类又持有了一个属性 root_target_definition,该属性引用了整个 TargetDefinition 树。因此,我们通过 Podfile 对象就可以得到所有相关内容。对此,Podfile 类提供了一个方法 to_hash 将所有的内容进行了合并,具体如下所示:

1
2
3
4
5
6
7
8
def to_hash
hash = {}
# 获取 TargetDefinition 树的所有内容
hash['target_definitions'] = root_target_definitions.map(&:to_hash)
# 将 TargetDefinition 树的所有内容与 internal_hash 进行合并
hash.merge!(internal_hash)
hash
end

依赖关系

至此,我们差不多能明白 Podfile 的工作原理:Podfile 本质上是描述了一个 Pods Project 及其所包含的一系列 Xcode Target,其中每一个 Xcode Target 都指定一个 pod 库构建对应 Product。由于 Demo Project 和 Pods Project 处于同一个 Xcode Workspace 中,因此 Demo Project 可以直接引用 Pods Project 构建的所有 Target,大体的依赖关系如下所示。

总结

本文以 Podfile 文件为入口,简单分析了 Podfile 文件的作用及其解析流程。在解析过程中,我们发现内部构建了一棵 TargetDefinition 树,树的层级结构正好与 Podfile 文件中的 target 嵌套结构相对应。每一个 TargetDefinition 节点存储了其所声明的 pod 依赖以及相关配置。Podfile 文件解析完成后会生成一个 Podfile 对象,该对象包含了 Podfile 文件声明的所有信息。

从本质上而言,Podfile 文件其实是描述了 Pods Project 的依赖以及配置。所有的第三方依赖都由 Pods Project 进行管理,主工程则是依赖 Pods Project 所构建的产物,从而有效地将主工程和第三方依赖进行了隔离。

参考

  1. CocoaPods-Core