CocoaPods 历险记 这个专题是 Edmond 和 冬瓜 共同撰写,对于 iOS / macOS 工程中版本管理工具 CocoaPods 的实现细节、原理、源码、实践与经验的分享记录,旨在帮助你们可以更加了解这个依赖管理工具,而不只局限于
pod install
和pod update
。html
本文知识目录

引子
在上文 「CocoaPods 命令解析 - CLAide」 中,咱们经过对 CLAide 的源码分析,了解了 CocoaPods 是如何处理 pod
命令,多级命令又是如何组织和嵌套的,并解释了命令行输出所表明的含义。今天咱们开始学习 Podfile
。ios
大多 iOS 工程师最早接触到的 CocoaPods 概念应该是 Podfile
,而 Podfile
属于 cocoapods-core
(如下简称 Core) 的两大概念之一。另一个则是 Podspec[2] (用于描述 Pod Library 的配置文件),只有当你须要开发 Pod 组件的时候才会接触。git
在介绍 Podfile 的内容结构以前,必需要谈谈 Xcode 的工程结构。github
Xcode 工程结构
咱们先来看一个极简 Podfile 声明:web
target 'Demo' do
pod 'Alamofire', :path => './Alamofire'
end
它编译后的工程目录以下:算法

如你所见 Podfile 的配置是围绕 Xcode 的这些工程结构:Workspace、Project、Target 以及 Build Setting 来展开的。做为包管理工具 CocoaPods 将所管理的 Pods 依赖库组装成一个个 Target,统一放入 Pods project
中的 Demo target
,并自动配置好 Target 间的依赖关系。swift
以后将 Example
主工程和 Pods
工程一块儿打包到新建的 Example.workspace
,配好主工程与 Pods
工程之间的依赖,完成最终转换。vim
接下来,咱们来聊一聊这些 Xcode 结构:数组
Target - 最小可编译单元
首先是 Target,它做为工程中最小的可编译单元,根据 Build Phases[3] 和 Build Settings[4] 将源码做为输入,经编译后输出结果产物。xcode
其输出结果能够是连接库、可执行文件或者资源包等,具体细节以下:
-
Build Setting:好比指定使用的编译器,目标平台、编译参数、头文件搜索路径等; -
Build 时的前置依赖、执行的脚本文件; -
Build 生成目标的签名、Capabilities 等属性; -
Input:哪些源码或者资源文件会被编译打包; -
Output:哪些静态库、动态库会被连接;
Project - Targets 的载体
Project 就是一个独立的 Xcode 工程,做为一个或多个 Targets 的资源管理器,自己没法被编译。Project 所管理的资源都来自它所包含的 Targets。特色以下:
-
至少包含一个或多个可编译的 Target; -
为所包含的 Targets 定义了一份默认编译选项,若是 Target 有本身的配置,则会覆盖 Project 的预设值; -
能将其余 Project 做为依赖嵌入其中;
下图为 Project 与所包含对 Targets 的关系

Workspace - 容器
做为纯粹的项目容器,Workspace 不参与任何编译连接过程,仅用于管理同层级的 Project,其特色:
-
Workspace 能够包含多个 Projects; -
同一个 Workspace 中的 Proejct 文件对于其余 Project 是默承认见的, 这些 Projcts 会共享 workspace build directory
; -
一个 Xcode Project 能够被包含在多个不一样的 Workspace 中,由于每一个 Project 都有独立的 Identity,默认是 Project Name;

Scheme - 描述 Build 过程
Scheme 是对于整个 Build 过程的一个抽象,它描述了 Xcode 应该使用哪一种 Build Configurations[5] 、执行什么任务、环境参数等来构建咱们所需的 Target。
Scheme 中预设了六个主要过程:Build、Run、Test、Profile、Analyze、Archive。包括了咱们对 Target 的全部操做,每个过程均可以单独配置。

CocoaPods-Core
CocoaPods-Core 用于 CocoaPods 中配置文件的解析,包括 Podfile
、Podspec
以及解析后的依赖锁存文件,如 Podfile.lock 等。
CocoaPods-Core 的文件构成
照例,咱们先经过入口文件 lib/cocoapods-core.rb
来一窥 Core 项目的主要文件:
module Pod
require 'cocoapods-core/gem_version'
class PlainInformative < StandardError; end
class Informative < PlainInformative; end
require 'pathname'
require 'cocoapods-core/vendor'
# 用于存储 PodSpec 中的版本号
autoload :Version, 'cocoapods-core/version'
# pod 的版本限制
autoload :Requirement, 'cocoapods-core/requirement'
# 配置 Podfile 或 PodSpec 中的 pod 依赖
autoload :Dependency, 'cocoapods-core/dependency'
# 获取 Github 仓库信息
autoload :GitHub, 'cocoapods-core/github'
# 处理 HTTP 请求
autoload :HTTP, 'cocoapods-core/http'
# 记录最终 pod 的依赖信息
autoload :Lockfile, 'cocoapods-core/lockfile'
# 记录 SDK 的名称和 target 版本
autoload :Platform, 'cocoapods-core/platform'
# 对应 Podfile 文件的 class
autoload :Podfile, 'cocoapods-core/podfile'
# 管理 PodSpec 的集合
autoload :Source, 'cocoapods-core/source'
# 管理基于 CDN 来源的 PodSpec 集合
autoload :CDNSource, 'cocoapods-core/cdn_source'
# 管理基于 Trunk 来源的 PodSpec 集合
autoload :TrunkSource, 'cocoapods-core/trunk_source'
# 对应 PodSpec 文件的 class
autoload :Specification, 'cocoapods-core/specification'
# 将 pod 信息转为 .yml 文件,用于 lockfile 的序列化
autoload :YAMLHelper, 'cocoapods-core/yaml_helper'
# 记录 pod 依赖类型,是静态库/动态库
autoload :BuildType, 'cocoapods-core/build_type'
...
Spec = Specification
end
将这些 Model 类按照对应的依赖关系进行划分,层级以下:

Podfile 的主要数据结构
先来了解 Podfile 的主要数据结构
Specification
Specification 即存储 PodSpec
的内容,是用于描述一个 Pod 库的源代码和资源将如何被打包编译成连接库或 framework,后续将会介绍更多的细节。
TargetDefinition
TargetDefinition
是一个多叉树结构,每一个节点记录着 Podfile
中定义的 Pod 的 Source 来源、Build Setting、Pod 子依赖等。
该树的根节点指向 Podfile
,而 Podfile
中的 root_target_definitions
则记录着全部的 TargetDefinition
的根节点,正常状况下该 list 中只有一个 root 即 Pods.project
。
为了便于阅读,简化了大量的 DSL 配置相关的方法和属性并对代码顺序作了调整,大体结构以下:
module Pod
class Podfile
class TargetDefinition
# 父节点: TargetDefinition 或者 Podfile
attr_reader :parent
# 子节点: TargetDefinition
attr_reader :children
# 记录 tareget 的配置信息
attr_accessor :internal_hash
def root?
parent.is_a?(Podfile) || parent.nil?
end
def root
if root?
self
else
parent.root
end
end
def podfile
root.parent
end
# ...
end
end
end
对应上一节 Xcode 工程结构中的 Podfile
关系以下:

CocoaPods 正是巧妙利用了 Xcode 工程结构的特色,引入 Pods.project
这一中间层,将主工程的 Pods 依赖所有转接到 Pods.project
上,最后再将 Pods.project
做为主项目的依赖。
尽管这么作也受到了一些质疑和诟病(所谓的侵入性太强),但笔者的观点是,正得益于 Pods.project
这一设计隔绝了第三方依赖库对于主项目的频繁更改,也便于后续的管理和更新,体现了软件工程中的 开放-关闭原则 。
好比,在 Pod 1.7.0 版本中支持的 Multiple Xcodeproj Generation[6] 就是解决随着项目的迭代而日益增大的 Pods
project 的问题。
试想当你的项目中存在上百个依赖库,每一个依赖库的变动都会影响到你的主工程,这将是很是可怕的问题。
Podfile
Podfile
是用于描述一个或多个 Xcode Project 中各个 Targets 之间的依赖关系。
这些 Targets 的依赖关系对应的就是 TargetDefinition
树中的各子节点的层级关系。如前面所说,有了 Podfile
这个根节点的指向,仅需对依赖树进行遍历,就能轻松获取完整的依赖关系。
有了这层依赖树,对于某个 Pod
库的更新便是对树节点的更新,即可轻松的分析出这次更新涉及的影响。
简化调整后的 Podfile 代码以下:
require 'cocoapods-core/podfile/dsl'
require 'cocoapods-core/podfile/target_definition'
module Pod
class Podfile
include Pod::Podfile::DSL
# podfile 路径
attr_accessor :defined_in_file
# 全部的 TargetDefinition 的根节点, 正常只有一个,即 Pods.project target
attr_accessor :root_target_definitions
# 记录 Pods.project 项目的配置信息
attr_accessor :internal_hash
# 当前 DSL 解析使用的 TargetDefinition
attr_accessor :current_target_definition
# ...
end
end
直接看 dsl.rb
,该文件内部定义了 Podfile DSL 支持的全部方法。经过 include 的使用将 Pod::Podfile::DSL
模块 Mix-in 后插入到 Podfile 类中。想了解更多 Mix-in 特性,移步 「CocoaPods 中的 Ruby 特性之 Mix-in」。
Lockfile
Lockfile
,顾名思义是用于记录最后一次 CocoaPods 所安装的 Pod 依赖库版本的信息快照。也就是生成的 Podfile.lock
。
在 pod install
过程,Podfile 会结合它来确认最终所安装的 Pod 版本,固定 Pod 依赖库版本防止其自动更新。
Lockfile
也做为 Pods 状态清单 (mainfest
),用于记录安装过程的中哪些 Pod 须要被删除或安装或更新等。
以开头的 Podfile 经 pod install
所生成的 Podfile.lock
为例:
PODS:
- Alamofire (4.6.0)
DEPENDENCIES:
- Alamofire (from `./Alamofire`)
EXTERNAL SOURCES:
Alamofire:
:path: "./Alamofire"
SPEC CHECKSUMS:
Alamofire: 0dda98a0ed7eec4bdcd5fe3cdd35fcd2b3022825
PODFILE CHECKSUM: da12cc12a30cfb48ebc5d14e8f51737ab65e8241
COCOAPODS: 1.10.0.beta.2
咱们来分析一下,经过该 Lockfile 可以获取哪些信息:
Key | 含义 |
---|---|
PODS | 记录全部 Pod 库的具体安装版本号 |
DEPENDENCIES | 记录各 Pod 库之间的相互依赖关系,因为这里只有 Alamofire 且它无其余依赖,暂时无关看出区别 |
EXTERNAL SOURCES | 记录部分经过外部源的 Pod 库(Git 引入、Path 引入) |
SPEC CHECKSUMS | 记录当前各 Pod 库的 Podspec 文件 Hash 值,其实就是文件的 md5 |
PODFILE CHECKSUM | 记录 Podfile 文件的 Hash 值,一样是 md5,确认是否有变动 |
COCOAPODS | 记录上次所使用的 CocoaPods 版本 |
Podfile
内容加载
Podfile
文件类型
你能够在 CocoaPods 的 /lib/cocoapods/config.rb
找到 Podfile 所支持的文件类型:
PODFILE_NAMES = [
'CocoaPods.podfile.yaml',
'CocoaPods.podfile',
'Podfile',
'Podfile.rb',
].freeze
CocoaPods 按照上述命名优先级来查找工程目录下所对应的 Podfile
文件。当发现目录中存在 CocoaPods.podfile.yaml
文件时会优先加载。
不少同窗可能只知道到 Podfile 支持 Ruby 的文件格式,而不了解它还支持了 YAML 格式。YAML 是 YAML Ain't Markup Language
的缩写,其官方定义[7]以下:
它是一种面向工程师友好的序列化语言。咱们的 Lockfile 文件就是以 YAML 格式写入 Podfile.lock
中的。
Podfile 文件读取
回到 lib/cocoapods-core/podfile.rb
来看读取方法:
module Pod
class Podfile
include Pod::Podfile::DSL
def self.from_file(path)
path = Pathname.new(path)
unless path.exist?
raise Informative, "No Podfile exists at path `#{path}`."
end
# 这里咱们能够看出,Podfile 目前已经支持告终尾是 .podfile 和 .rb 后缀的文件名
# 实际上是为了改善不少编译器使用文件后缀来确认 filetype,好比 vim
# 相比与 Podfile 这个文件名要更加的友好
case path.extname
when '', '.podfile', '.rb'
Podfile.from_ruby(path)
when '.yaml'
# 如今也支持了 .yaml 格式
Podfile.from_yaml(path)
else
raise Informative, "Unsupported Podfile format `#{path}`."
end
end
end
from_file
在 pod install
命令执行后的 verify_podfile_exists!
中被调用的:
def verify_podfile_exists!
unless config.podfile
raise Informative, "No `Podfile' found in the project directory."
end
end
而 Podfile
文件的读取就是 config.podfile
里触发的,代码在 CocoaPods 的 config.rb
文件中:
def podfile_path_in_dir(dir)
PODFILE_NAMES.each do |filename|
candidate = dir + filename
if candidate.file?
return candidate
end
end
nil
end
def podfile_path
@podfile_path ||= podfile_path_in_dir(installation_root)
end
def podfile
@podfile ||= Podfile.from_file(podfile_path) if podfile_path
end
这里的方法 podfile
和 podfile_path
都是 lazy 加载的。最后 Core 的 from_file
将依据目录下的 Podfile
文件类型选择调用 from_yaml
或者 from_ruby
。
从 Pod::Command::Install
命令到 Podfile 文件加载的调用栈以下:

Podfile From Ruby 解析
当咱们经过 pod init
来初始化 CocoaPods 项目时,默认生成的 Podfile 名称就是 Podfile
,那就从 Podfile.from_ruby
开始。
def self.from_ruby(path, contents = nil)
# ①
contents ||= File.open(path, 'r:utf-8', &:read)
# 兼容 1.9 版本的 Rubinius 中的编码问题
if contents.respond_to?(:encoding) && contents.encoding.name != 'UTF-8'
contents.encode!('UTF-8')
end
# 对 Podfile 中不规范的单引号或双引号进行检查,并进行自动修正,及抛出错误
if contents.tr!('“”‘’‛', %(""'''))
CoreUI.warn "..."
end
# ②
podfile = Podfile.new(path) do
begin
eval(contents, nil, path.to_s)
rescue Exception => e
message = "Invalid `#{path.basename}` file: #{e.message}"
raise DSLError.new(message, path, e, contents)
end
end
podfile
end
① 是对 Podfile 内容的读取和编码,同时对可能出现的单引号和双引号的匹配问题进行了修正。② 以 path
和 block
为入参进行 podfile
类的初始化并将其放回,保存在全局的 config.podfile
中。
Tips: 若是要在 Ruby 对象的初始化中传入参数,须要重载 Object 的 initialize[8] 方法,这里的 Podfile.new(...) 本质上是
initialize
的方法调用。
initialize
方法所传入的尾随闭包 block
的核心在于内部的 eval
函数(在 CocoaPods 核心组件[9] 中有提到):
eval(contents, nil, path.to_s)
它将 Podfile 中的文本内容转化为方法执行,也就是说里面的参数是一段 Ruby 的代码字符串,经过 eval
方法能够直接执行。继续看 Podfile 的 initialize
方法:
def initialize(defined_in_file = nil, internal_hash = {}, &block)
self.defined_in_file = defined_in_file
@internal_hash = internal_hash
if block
default_target_def = TargetDefinition.new('Pods', self)
default_target_def.abstract = true
@root_target_definitions = [default_target_def]
@current_target_definition = default_target_def
instance_eval(&block)
else
@root_target_definitions = []
end
end
它定义了三个参数:
参数 | 定义 |
---|---|
defined_in_file |
Podfile 文件路径 |
internal_hash |
经过 yaml 序列化获得的 Podfile 配置信息,保存在 internal_hash 中 |
block |
用于映射 Podfile 的 DSL 配置 |
须要注意的是,经过
from_ruby
初始化的Podfile
只传入了参数 1 和 3,参数 2internal_hash
则是提供给from_yaml
的。
当 block
存在,会初始化名为 Pods
的 TargetDefinition 对象,用于保存 Pods project
的相关信息和 Pod 依赖。而后调用 instance_eval[10] 执行传入的 block
,将 Podfile 的 DSL 内容转换成对应的方法和参数,最终将参数存入 internal_hash
和对应的 target_definitions
中。
Tips: 在 Ruby 中存在两种不一样的方式来执行代码块
block
,分别是instance_eval
和class_eval
。class_eval
的执行上下文与调用类相关,调用者是类名或者模块名,而instance_eval
的调用者能够是类的实例或者类自己。细节看 StackOverFlow[11]。
Podfile From YAML 解析
YAML 格式的 Podfile 加载须要借助 YAMLHelper 类来完成,YAMLHelper 则是基于 yaml[12] 的简单封装。
def self.from_yaml(path)
string = File.open(path, 'r:utf-8', &:read)
# 为了解决 Rubinius incomplete encoding in 1.9 mode
# https://github.com/rubinius/rubinius/issues/1539
if string.respond_to?(:encoding) && string.encoding.name != 'UTF-8'
string.encode!('UTF-8')
end
hash = YAMLHelper.load_string(string)
from_hash(hash, path)
end
def self.from_hash(hash, path = nil)
internal_hash = hash.dup
target_definitions = internal_hash.delete('target_definitions') || []
podfile = Podfile.new(path, internal_hash)
target_definitions.each do |definition_hash|
definition = TargetDefinition.from_hash(definition_hash, podfile)
podfile.root_target_definitions << definition
end
podfile
end
经过 from_yaml
将文件内容转成 Ruby hash 后转入 from_hash
方法。
区别于 from_ruby
,这里调用的 initialize
将读取的 hash 直接存入 internal_hash
,而后利用 TargetDefinition.from_hash
来完成的 hash 内容到 targets 的转换,所以,这里无需传入 block 进行 DSL 解析和方法转换。
Podfile 内容解析
前面提到 Podfile 的内容最终保存在 internal_hash
和 target_definitions
中,本质上都是使用了 hash
来保存数据。因为 YAML 文件格式的 Podfile 加载后就是 hash 对象,无需过多加工。惟一须要处理的是递归调用 TargetDefinition 的 from_hash
方法来解析 target 子节点的数据。
所以,接下来的内容解析主要针对 Ruby 文件格式的 DSL 解析,咱们以 pod
方法为例:
target 'Example' do
pod 'Alamofire'
end
当解析到 pod 'Alamofire'
时,会先经过 eval(contents, nil, path.to_s
将其转换为 dsl.rb
中的方法:
def pod(name = nil, *requirements)
unless name
raise StandardError, 'A dependency requires a name.'
end
current_target_definition.store_pod(name, *requirements)
end
name 为 Alamofire,因为咱们没有指定对应的 Alamofire 版本,默认会使用最新版本。requirements
是控制 该 pod 来源获取或者 pod target 的编译选项等,例如:
pod 'Alamofire', '0.9'
pod 'Alamofire', :modular_headers => true
pod 'Alamofire', :configurations => ['Debug', 'Beta']
pod 'Alamofire', :source => 'https://github.com/CocoaPods/Specs.git'
pod 'Alamofire', :subspecs => ['Attribute', 'QuerySet']
pod 'Alamofire', :testspecs => ['UnitTests', 'SomeOtherTests']
pod 'Alamofire', :path => '~/Documents/AFNetworking'
pod 'Alamofire', :podspec => 'https://example.com/Alamofire.podspec'
pod 'Alamofire', :git => 'https://github.com/looseyi/Alamofire.git', :tag => '0.7.0'
Tips:requirements 最终是以 Gem::Requirement 对象来保存的。关于 pod 详细说明请移步:Podfile 手册[13]。
对 name 进行校验后,直接转入 current_target_definition
毕竟 Pod 库都是存在 Pods.project
之下:
def store_pod(name, *requirements)
return if parse_subspecs(name, requirements) # This parse method must be called first
parse_inhibit_warnings(name, requirements)
parse_modular_headers(name, requirements)
parse_configuration_whitelist(name, requirements)
parse_project_name(name, requirements)
if requirements && !requirements.empty?
pod = { name => requirements }
else
pod = name
end
get_hash_value('dependencies', []) << pod
nil
end
def get_hash_value(key, base_value = nil)
unless HASH_KEYS.include?(key)
raise StandardError, "Unsupported hash key `#{key}`"
end
internal_hash[key] = base_value if internal_hash[key].nil?
internal_hash[key]
end
def set_hash_value(key, value)
unless HASH_KEYS.include?(key)
raise StandardError, "Unsupported hash key `#{key}`"
end
internal_hash[key] = value
end
通过一系列检查以后,调用 get_hash_value
获取 internal_hash
的 dependencies
,并将 name 和 requirements
选项存入。
这里的 dependencies
key 是定义在 TargetDefinition 文件的 HASH_KEYS
,表示 Core 所支持的配置参数:
# freeze 表示该数组不可修改。另外,%w 用于表示其中元素被单引号括起的数组。
# %W(#{foo} Bar Bar\ with\ space) => ["Foo", "Bar", "Bar with space"]
# 对应的还有 %W 表示其中元素被双引号括起的数组。
HASH_KEYS = %w(
name
platform
podspecs
exclusive
link_with
link_with_first_target
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
整个映射过程以下:

精细化的 Podfile 配置
最后一节让咱们来 Show 一下 ,看看 Podfile
所谓的 targets
之间的依赖关系能够玩出什么花来 😂 。
Target 嵌套
最简单的 Podfile
就是文章开头所展现的,不过在 Podfile
中还能够对 Target 进行嵌套使用。
假设在咱们的主工程同时维护了三个项目,它们都依赖了 Alamofire,经过俄罗斯套娃就能轻松知足条件:
target 'Demo1' do
pod 'Alamofire'
target 'Demo2' do
target 'Demo3' do
end
end
end
编译后的 Pods.project
项目结构以下:

咱们知道,CocoaPods 在 Pods.project
中为每一个在 Podfile 中声明的 Target 生成一个与之对应的专属 Target 来集成它的 Pod 依赖。
对于有依赖关系的 Target 其生成的专属 Target 名称则会按照依赖关系叠加来命名,如 target Demo3
的专属 Target 名称为 Pods-Demo1-Demo2-Demo3。安装完成后主项目将会引入该专属 Target 来完成依赖关联,如 Demo3:

关于 Target 嵌套,一个父节点是能够有多个子节点的:
target 'Demo1' do
pod 'Alamofire'
target 'Demo2' do
pod 'RxSwift'
end
target 'Demo3' do
pod 'SwiftyJSON'
end
end
Abstract Target
上面例子中,因为 Demo1 与 Demo2 都须要依赖 Alamofire,咱们经过 Target 嵌套让 Demo2 来继承 Demo1 的 Pods 库依赖。
这么作可能会有一个限制,就是当 Demo1 的 Pod 依赖并不是所有为 Demo2 所须要的时候,就会有依赖冗余。此时就须要 Abstract Target
登场了。例如:
abstract_target 'Networking' do
pod 'Alamofire'
target 'Demo1' do
pod 'RxSwift'
end
target 'Demo2' do
pod 'ReactCocoa'
end
target 'Demo3' do
end
end
将网络请求的 Pod 依赖抽象到 Networking
target 中,这样就能避免 Demo2 对 RxSwift 的依赖。
这种方式配置所生成的 Pods.project
并不会存在名称为 Networking
的 Target,它仅会在主工程的专属 Target 中留下印记:

总结
本文结合 Xcode 工程结构来展开 CocoaPods-Core 的 Podfile 之旅,主要感觉以下:
-
再一次感觉了 Ruby 语言的动态之美,给我一个字符串,还你一个未知世界; -
结合 Xcode 工程结构更好的理解了 Podfile 的设计初衷, 基础知识很重要; -
所谓“算法无用论”这种事情,在计算机的世界是不存在的,没有好的数据结构知识如何更好的抽象; -
了解 Podfile 的 DSL 是如何映射到内存中,又是如何来存储每一个关键数据的
知识点问题梳理
这里罗列了四个问题用来考察你是否已经掌握了这篇文章,若是没有建议你加入 收藏 再次阅读:
-
说说 TargetDefinition
的数据结构 ? -
说说 TargetDefinition
与 Xcode Project 的关系 ? -
Podfile
的文件格式有几种,分别是如何加载 ? -
Lockfile
和Podfile
的关系
参考资料
CocoaPods 命令解析: https://zhuanlan.zhihu.com/p/212101448
[2]Podspec
: https://guides.cocoapods.org/syntax/podspec.html
Build Phases: https://www.objc.io/issues/6-build-tools/build-process/#controlling-the-build-process
[4]Build Settings: https://developer.apple.com/library/archive/featuredarticles/XcodeConcepts/Concept-Build_Settings.html#//apple_ref/doc/uid/TP40009328-CH6-SW1
[5]Build Configurations: https://medium.com/practical-ios-development/some-practical-uses-for-xcode-build-schemes-and-build-configurations-swift-e50d15a1304f
[6]Multiple Xcodeproj Generation: http://blog.cocoapods.org/CocoaPods-1.7.0-beta/
[7]官方定义: https://yaml.org/
[8]initialize: https://ruby-doc.org/docs/ruby-doc-bundle/UsersGuide/rg/objinitialization.html
[9]CocoaPods 核心组件: https://zhuanlan.zhihu.com/p/187272448
[10]instance_eval: https://ruby-doc.org/core-2.7.0/BasicObject.html
[11]StackOverFlow: https://stackoverflow.com/questions/900419/how-to-understand-the-difference-between-class-eval-and-instance-eval
[12]yaml: https://github.com/ruby/yaml
[13]Podfile 手册: https://guides.cocoapods.org/syntax/podfile.html#pod
本文分享自微信公众号 - 一瓜技术(tech_gua)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。