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 | source 'https://cdn.cocoapods.org/' |
Podfile 初始化
在上述例子中,Podfile
文件声明了项目的依赖,诸如:Alamofire
、Dollar
、SnapKit
等。当我们执行 pod install
命令时,CocoaPods 会根据 Podfile
文件路径进行初始化,如下所示:
1 | # config.rb |
Podfile
类提供了类方法 self.from_file
,根据
Podfile 文件路径进行初始化。self.from_file
方法的定义如下所示:
1 | def self.from_file(path) |
在 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 | def self.from_ruby(path, contents = nil) |
在工厂方法中,主要通过 3 个步骤来初始化 Podfile。
- 首先,读取 Podfile 文件的内容,并进行 UTF-8 编码。
- 然后,检测文件内容是否存在非法字符,如果存在则抛出异常。
- 最后,通过
Podfile
类构造方法进行初始化,并传入一个尾随闭包。
注意,最后一步的尾随闭包包含了一行关键代码:eval(contents, nil, path.to_s)
。正是这行代码,执行了我们在 Podfile
文件声明的所有内容,从而使它们正式生效。我们在 Podfile
文件进行声明,本质上是编写了对一系列 ruby 方法的调用。
TargetDefinition 树构建
我们继续深入 Podfile
类的构造方法,其定义如下所示:
1 | def initialize(defined_in_file = nil, internal_hash = {}, &block) |
对于 ruby
类型的 Podfile 文件,Podfile
对象初始化时,会构建一个 TargetDefinition
树的根节点。然后,通过 instance_eval
方法真正执行 Podfile
中的代码。从而在已有的 TargetDefinition
根节点的基础上,进一步构建了整棵 TargetDefinition
树。
在 Demo Project 的例子中,Podfile
初始化后,构建的
TargetDefinition
树如下图所示:
TargetDefinition
树的层级结构与 Podfile 文件中声明的
target
嵌套层级结构相关联,如下为 Podfile 文件中
target
的定义。
1 | target 'Demo' do |
我们来看一下 target
方法的具体实现,如下所示:
1 | def target(name, options = nil) |
在 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 | module Pod |
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 | HASH_KEYS = %w( |
对比一下 Xcode Target
的结构类图,如下所示,我们大概能找到一些对应关系,尤其是
PBXNativeTarget
,如:dependencies
、build_configurations
、script_phases
等。
DSL 解析
在 TargetDefinition
树构造过程中,我们用到的
target
方法定义在 Podfile DSL 中。除此之外,Podfile DSL
中还定义了一系列方法。这些 DSL
的主要作用是:为开发者提供简洁的语法,从而支持自定义配置
Target、Project、Workspace,并且提供了一系列 hook
方法,允许开发者执行自定义脚本。
Podfile DSL 本质上是一个 module,它会被 mixin 到 Podfile
类中,其内部实现与 Podfile
紧密关联。尤其是,Podfile DSL
使用了 Podfile
类中定义的
current_target_definition
和
internal_hash
,用于存储 Podfile 所声明的配置信息。
Podfile DSL 根据功能可以分为如下几种类型:
- Root Options
- Dependencies
- Target Configuration
- Workspace
- Sources
- Hooks
下面,对这些 DSL 进行简要的介绍。
Root Options
Root Options 相关的 DSL 主要用于对 Podfile 进行整体配置,如下所示:
1 | # 声明安装过程中会使用的安装方法和选项 |
Dependencies
Dependencies 相关的 DSL 主要用于为每个 Target
指定依赖。其主要通过调用 TargetDefinition
类所提供的方法将配置存入 TargetDefinition
类的
internal_hash
中。相关 DSL 如下所示:
1 | # 创建一个特定依赖 |
Target Configurations
Target Configurations 相关的 DSL 主要用于控制 CocoaPods 生成 Project
文件。类似的,其主要也通过调用 TargetDefinition
类所提供的相关方法将配置存入 TargetDefinition
类的
internal_hash
中。相关 DSL 如下所示:
1 | # 设置当前 target 的目标平台 |
Workspace
Workspace 相关的 DSL 主要用于配置 Xcode Workspace,设置全局的
settings。其将配置存储在 Podfile
类的
internal_hash
中。相关 DSL 如下所示:
1 | # 设置包含所有 project 的 Xcode workspace |
Sources
Sources 相关的 DSL 主要用于设置源,Podfile 通过源列表查找对应的 Specification。相关的 DSL 如下所示:
1 | # 指定 spec 的位置 |
Hooks
Hooks 相关的 DSL 主要用于配置自定义脚本。Podfile 提供了一系列 hooks,这些 hooks 会在 install 过程中被执行。相关的 DSL 如下所示:
1 | # 设置安装过程中会用到的插件 |
Podfile 对象
当 Podfile 文件中的代码执行完毕后,Podfile
对象也就构建完成了。Podfile
类的主要定义如下所示:
1 | module Pod |
类似于 TargetDefinition
,Podfile
也提供了一个 internal_hash
用于存储一些全局的设置,它通过一个 HASH_KEYS
数组预定义了一系列键,如下所示:
1 | HASH_KEYS = %w( |
在 DSL 解析一节中,我们知道了 DSL 将配置信息分别存储在了
TargetDefinition
类的 internal_hash
和
Podfile
类的 internal_hash
中。而
Podfile
类又持有了一个属性
root_target_definition
,该属性引用了整个
TargetDefinition
树。因此,我们通过 Podfile
对象就可以得到所有相关内容。对此,Podfile
类提供了一个方法
to_hash
将所有的内容进行了合并,具体如下所示:
1 | def to_hash |
依赖关系
至此,我们差不多能明白 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 所构建的产物,从而有效地将主工程和第三方依赖进行了隔离。