Cocoapods 源码浅析 - 从 pod 命令开始

当看到这篇博客,相信你已经不再困惑与 Cocoapods 如何使用,而是 Cocoapods 是如何实现的。我也有这样的好奇,其实 Cocoapods 源码一直在断断续续的读,决定好好梳理一下。如果时间有限,可以直接跳到最后的 Q & A。

环境信息
Cocoapods 1.9.0.beta.2 (f1293b7)
Core 1.9.0.beta.2 (95e133d)


目标

先从最简单的日常使用来看,比如项目要引入 AFNetworking

  1. 执行 pod init 生成 Podfile(或者自己创建一个 Podfile)
  2. 书写 Podfile
  3. 执行 pod install(除此之外,还有一个命令叫 pod update
  4. 打开 .xcworkspace,结束

比较感兴趣的是:

  1. Podfile 是什么?(之前写了一篇 Podfile 解析最佳实践 有提到 Podfile 是什么,以及如何自己解析 Podfile,所以这篇文章不会再提)
  2. pod install 做了什么事情?
  3. .xcworkspace 是如何生成的?
  4. pod installpod update 有什么区别?
  5. 生成的新文件中,还有一个叫 Podfile.lock,它的作用是什么?

Cocoapods 项目

Cocoapods 的代码并不全都在 Cocoapods/Cocoapods 项目中,而是拆分为了多个 Repo。在 Cocoapods README 的最后,对这些项目进行了简述:

建议除了 Cocoapods.app 和 Master repository,其他仓库都 clone 下来。

如何 Debug Cocoapods

  • 将以上仓库都 clone 下来,Cocoapods 项目作为入口,修改 Cocoapods/Gemfile,指定每个依赖的 path
  • Cocoapods 目录下,执行 bundle install
  • 进入 Cocoapods/examples/AFNetworking Example
  • 执行 bundle exec pod install,后续指令都带上 bundle exec 即可

如何验证:在 Cocoapods/lib/cocoapods/command.rbself.run 中新增调用栈输出的代码:

def self.run(argv)
# 新增输出调用栈的代码
puts caller

help! 'You cannot run CocoaPods as root.' if Process.uid == 0 && !Gem.win_platform?

verify_minimum_git_version!
verify_xcode_license_approved!

super(argv)
ensure
UI.print_warnings
end

在 Example 中执行 bundle exec pod install,能看到调用栈打印,就没问题。

命令如何解析

Cocoapods/lib/commands 文件夹,不难看出具体命令的实现,比如 pod install 对应为 Cocoapods/lib/commands/install.rb。全局搜索 Pod::Command::Install 没有有效信息,大概率是动态调用。在 Install 的 run 方法中打印调用栈,会发现顶调用栈在 CLAide/lib/claide/command.rb 中。

Install 对象初始化在 CLAide 的 parse 方法中,根据传入的参数(如 install)生成对应的类。

pod install

installer 初始化与执行:

# sandbox: Pods 对象(Pods 文件夹),在 sandbox.rb 中有详细目录结构注释
# podfile: podfile 对象(源码在 Core 中),生成 podfile 对象的时候,就已经解析完 Podfile 文件了
# lockfile: podfile.lock 对象(源码在 Core 中)
Installer.new(config.sandbox, config.podfile, config.lockfile)


# Podfile 解析,Core/lib/cocoapods-core/podfile.rb
# Podfile DSL 定义在 Core/lib/cocoapods-core/podfile/dsl.rb
include Pod::Podfile::DSL
# 执行 Podfile
eval(contents, nil, path.to_s)


# 如果好奇 Podfile 解析以后的结构,可以在 installer 初始化方法中输出:
puts @podfile.to_hash


# install 方法调用
installer.install!

Cocoapods/lib/cocoapods.rbautoload :Installer, 'cocoapods/installer',所以 Installer.newPod::Installer.new
如果 installer 中部分调用找不到文件路径,可以看 installer 中 autoload 对应的路径

prepare

  1. 如果前后 pod 主版本号不一致,清理旧版本 pod 生成的结构。比如 pod 从 1.x 升级至 2.x,生成的工程结构可能有大的变化
  2. 安装 Podfile 指定的插件
  3. 执行插件注册的 pre install hook

prepare 这一步还会创建一些必要的文件夹,完成以后,目录是:

- Podfile
- Pods
- Headers
- Local Podspecs
- Target Support Files
- Example
- Example.xcodeproj

resolve_dependencies

依赖解析内容比较多(比如如何计算具体版本,如何解析依赖关系等),后续单独整理。在 installer 中,去除不必要的代码如下:

def resolve_dependencies
# 读取 plugin 中注册了 :sources_provider 的 hook,具体作用还没细看,后续整理到 cocoapods plugin 的部分
# 常用的一些 plugin 暂时没看到这个 hook,这里先忽略不会影响依赖分析的理解
plugin_sources = run_source_provider_hooks
analyzer = create_analyzer(plugin_sources)

# pod install --repo-update
# 先更新 repos,然后再解析依赖
if repo_update?
analyzer.update_repositories
end

# 依赖解析,解析完以后,将结果赋值给 @analysis_result
analyze(analyzer)
# 校验 pod 白名单的 configuration 的有效性
# 比如:pod "AFNetworking", "1.3.3", :configurations => ['Debug123'] 的 Debug123 是否存在
validate_build_configurations

# pod install --deployment
# 禁止在 install 过程中变更 Podfile/Podfile.lock
if deployment?
verify_no_podfile_changes!
verify_no_lockfile_changes!
end

analyzer
end

解析完成后,@analysis_result 都包含什么:

  • 相较于上一次,pod 的变更。如:add/delete 的 pod 等
  • { TargetDefinition: [ResolverSpecification] } 的映射,也就是 target 下对应的依赖数组(这里已经确定下来依赖应该用的版本号了)
  • 依赖需要生成的 target,比如 AFNetworking 有 macOS 和 iOS 两个 Target(注意是 Pods 工程里面依赖的 target,不是 App 的 target)
  • 依赖关系缓存

download_dependencies

依赖下载,这部分内容也比较多(如预下载,缓存查找,缓存策略等),后续单独整理。主要分为三步:

  1. 根据依赖分析,找到 add/change 的 pod,构造下载器
  2. 判断 ~/Library/Caches/CocoaPods/Pods/ 下是否有缓存,未命中缓存,调用 Downloader.download_request 执行下载
  3. 执行 Podspec 中的 prepare_command(可用于编辑已下载的文件,执行目录是 Demo/Pods/PodName
  4. 执行 Podfile 中,挂载的 pre_install 钩子,并把当前 installer 传进去
  5. 清理多余的文件,比如没有依赖到的类等等

下载完成后,所需资源(源码/二进制)就放在 Pods 文件夹中:

- Podfile
- Pods
- AFNetworking
- Headers
- Local Podspecs
- Target Support Files
- Example
- Example.xcodeproj

integrate

经历了以上步骤,依赖的代码,所需的 target 都已经解析、下载完毕,剩下需要将依赖进行整合,生成 Pods.xcodeproj。调用入口在 Cocoapods/lib/cocoapods/installer.rbcreate_and_save_projects

生成 .xcodeproj

依托于 Cocoapods 封装的 Xcodeproj 组件,要生成 .xcodeproj 是非常容易的。比如以下是一段可以直接执行,用于生成 .xcodeproj 的代码(Xcodeproj::Project 接口说明):

require 'xcodeproj'

def create_project(path)
# 初始化 proj 文件
proj = Xcodeproj::Project.new(path, false, 46)
# 创建 Targets Support Files Group
proj.new_group('Targets Support Files')
# 创建 Pods group
proj.new_group('Pods')
# 生成持久化文件(保存在指定的 path 下)
proj.save
end

path = File.join(__dir__, 'Pods', 'Pods.xcodeproj')
create_project(path)

这段代码生成了一个空的 Pods.xcodeproj 与几个 group(还未生成 target):

关联文件

AFNetworking 下载以后,以文件的形式放在 Pods/AFNetworking 文件夹中,其中一部分文件是需要关联到工程中的(需要变更 .xcodeporj,准确点是变更 project.pbxproj,不明白可以拖一个文件到工程里面,看看前后 project.pbxproj 的变化)。

所以这一步需要解决的是哪些文件,应该放到哪个位置。影响的因素有:pod 名称、subspce 名称、资源类型等,比如 AFURLSessionManager.h 的 group 为:

{
"isa" => "PBXGroup",
"sourceTree" => "<group>",
"name" => "NSURLSession",
"children" => [
[0] "46EB2E000000F0",
[1] "46EB2E00000100"
]
}

生成 Pod Target

一个 Pod 并不是对应着一个 Target,拿 AFNetworking 来说,会生成组件同名的 AFNetworking Target。如果是包含资源文件的 Pod,还会生成与资源同名的 bundle 的 target。这部分代码在 Cocoapods/lib/cocoapods/installer/xcode/pods_project_generator/pod_target_generator.rb 中。

resource_bundle

如果组件中的资源是以 resource_bundle 的形式声明的,会在这一步添加 resource_bundle。

Pod::Spec.new do |s|
s.name = "MyPod"
s.resource_bundles = {"MyPodImages" => ["images/*.jpg"] }
end
  1. 生成 resource bundle target
  2. 添加指定的资源到 resource bundle target
  3. 生成 bundle 资源对应的 info.plist
  4. 生成 configurations

build_phases

这一步主要处理编译步骤中的相关文件,如 Headers/Compile Sources 等。Headers 需要区分 Public/Private/Project,Compile Source 需要处理 arc/non arc 等。

xcconfig_file

xccofig 文件用于环境变量写入,每一项都可以在 BuildSettings 的 User-Defined 中看到。通常遇到的「AppStore 渠道需要不同的配置」,「版本号自动升级」,「BuildSettings 变更」等,可以设想在复杂的工程开发下,xcconfig 可以使复杂的 .xcodeproj 变更简化为单个 .xcconfig 文件的变更,并且读写更清晰。介绍推荐 Xcode Build Configuration Files,一些高阶用法,推荐 The Unofficial Guide to xcconfig files

在之前的 Target 生成中,会变更一些 BuildSettings;又或者,某些 pod 有自己的宏,在大家都改 Build Settings 的情况下,打开 Xcode 会发现完全找不到有哪些修改。在这一步中,Cocoapods 将这些变更导出为了 .xcconfig 文件。

# podspec 中,给 Pod 新增宏
Pod::Spec.new do |s|
s.name = "MyPod"
s.xcconfig = {'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) COCOAPODS=1 MY_POD=1' }
end

# pod 默认的一些 build settings(Cocoapods/lib/cocoapods/target/build_settings.rb)
# @return [String]
define_build_settings_method :skip_install, :build_setting => true do
'YES'
end

# @return [String]
define_build_settings_method :product_bundle_identifier, :build_setting => true do
'org.cocoapods.${PRODUCT_NAME:rfc1034identifier}'
end

最终生成的 .xcconfig 文件位于 Demo/Pods/Target Support Files/AFNetworking/AFNetworking.debug.xcconfig

CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/MyPod
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 MY_POD=1
PODS_BUILD_DIR = ${BUILD_DIR}
SKIP_INSTALL = YES

生成 .xcconfig 文件后,将文件关联到工程中,并且 Resource Bundle 也关联 pod 的 xcconfig。

module_map 与 umbrella_header

如果在 Podfile 中 use_framework! 或 Swift(也或者声明了 DEFINES_MODULE = YES 的静态库),则会多出 create_module_mapcreate_umbrella_header 这两步。

什么是 module map
在 C 系列语言中,头文件通过 #include <SomeLib.h> 的方式引入,但这种方式存在诸多问题:

  • 编译耗时:每 include 一次,编译器在预处理的时候,就要解析一次。如果有 N 个编译单元中包含 M 个 Header,即使这 M 个 Header 是共享的,编译器也会处理 N * M 次;
  • 脆弱:include 会作为文本文件处理,在遇到宏定义冲突时,报错关系混乱(根据引入先后顺序,可能报到标准库中);
  • 解决方案丑陋:为了避免宏定义冲突,开发者们不得不用一长串大写+下划线+各种前缀的方式来定义一个宏;
  • 工具混乱:在不同的编译器下,哪些库属于标准库,用什么顺序引入这些头文件才能编过,都不统一。

针对于这些问题,LLVM 提出了 module 的概念,而 module map 就是用于描述 module 与 headers 之间映射关系的文件。而 Cocoapods 中自动生成了 module map,所以可以直接用 @import SomeLib 来引入 module。

AFNetworking module map(Demo/Pods/Target Support Files/AFNetworking/AFNetworking.modulemap)就是根据 Pod::Generator::ModuleMap.generate 提供的模板生成的:

framework module AFNetworking {
umbrella header "AFNetworking-umbrella.h"

export *
module * { export * }
}

除了将文件配置同步到 .xcodeproj 文件中以外,还需要在 Build Settings 中,设置 MODULEMAP_FILE.modulemap 的路径。

在 module map 中指定了 umbrella header 是 AFNetworking-umbrella.h,所以下一步是生成 umbrella header。

什么是 umbrella header
umbrellaheader 都是 module map 语法定义的关键字。如果 header 用 umbrella 修饰,说明被修饰的 header 包含了子目录下的所有头文件。在 #include 中,umbrella header 一次性引入某个库下面的所有头文件,方便其他库调用。在 module 中,用于整合所有 header,无需每个 header 都有自己的 module。给定目录中,只能有一个 umbrella header。
比如 AFNetworking 就只需要 @import AFNetworking;

在读取 AFNetworking 所有 public header 以后,通过 umbrella header 的模板生成 header 文件(省略了一些):

# Cocoapods/lib/cocoapods/generator/header.rb
def generate
result = ''
result << "#ifdef __OBJC__\n"
result << "#else\n"
result << "#ifndef FOUNDATION_EXPORT\n"
result << "#endif\n"
result << "\n"
# 拼接 #import "header.h"
imports.each do |import|
result << %(#import "#{import}"\n)
end
end

除了 module map 与 umbrella header,后续还有针对 macOS 与 Swift 的一些处理,暂不展开。

pch

根据系统(iOS/macOS/watchOS/tvOS)和 pod 指定的 pch,生成 pch 文件,并修改 Build Settings 的 GCC_PREFIX_HEADER。podspec prefix_header_file 可声明为:

Pod::Spec.new do |s|
s.name = "MyPod"
s.prefix_header_file = 'iphone/include/prefix.pch'
end

dummy_source

为了解决 pod 仅有 Category 没有 Object 导致的链接不过问题,Cocoapods 在这一步给 pod 创建了 dummy 文件。其中声明了 PodsDummy_AFNetworking 类(Demo/Pods/Target Support Files/AFNetworing/AFNetworking-dummy.m):

#import <Foundation/Foundation.h>
@interface PodsDummy_AFNetworking : NSObject
@end
@implementation PodsDummy_AFNetworking
@end

add_system_framework_dependencies

根据 podspec 指定的 frameworks,添加系统依赖。

生成 aggregate Target

如果打开 Pods.xcodeproj 文件,会发现 Target 分为三类:pod target,resource bundle target,还有一个加了 Pods- 前缀并且和工程同名的 Target。Cocoapods 把这个 Target 称为 aggregate target,字面意思就是「总的 Target」。源码与单个 Pod Target 的 pod_target_generator.rb 同级,名为 aggregate_target_installer.rb

installation_options

在整个 install 过程中,都不难发现 installation_options 的身影。很多参数都从这个 Options 里面读取,那么这个参数到底从哪里来?又都包含哪些配置呢?

install 中的几次排序

  1. 依赖下载时,按名称排序
  2. target 生成时,按名称排序
  3. pod target 中,Headers 与 Compile Sources,按名称排序

Q & A

粗略的看完 pod install 过程,一些疑问也得到解答。

install 的都包含了哪些流程?

Pods 目录下都有什么?

出现 install 后,符号重复或符号缺失问题,怎么处理?