2. 整体把握 CocoaPods 核心组件
CocoaPods历险记这个专题是Edmond和冬瓜共同撰写,对于 iOS / macOS 工程中版本管理工具 CocoaPods 的实现细节、原理、源码、实践与经验的分享记录,旨在帮助大家能够更加了解这个依赖管理工具,而不仅局限于pod install和pod update。
本文知识目录
引子
在上文 版本管理工具及 Ruby 工具链环境 中,我们聊到如何统一管理团队小伙伴的 CocoaPods 生产环境及使用到的 Ruby 工具链。今天让我们将目光转到 CocoaPods 身上,一起来聊聊它的主要构成,以及各个组件在整个 Pods 工作流的关系。
为了整体把握 CocoaPods 这个项目,建议大家去入门一下 Ruby 这门脚本语言。另外本文基于 CocoaPods 1.9.2 版本。
CocoaPods 的核心组件
作为包管理工具,CocoaPods 随着 Apple 生态的蓬勃发展也在不断迭代和进化,并且各部分核心功能也都演化出相对独立的组件。这些功能独立的组件,均拆分出一个个独立的 Gem 包,而 CocoaPods 则是这些组件的“集大成者”。
CocoaPods 依赖总览
我们知道在 Pod 管理的项目中,Podfile
文件里描述了它所依赖的 dependencies,类似的 Gem 的依赖可以在 Gemfile
中查看。那 CocoaPods 的 Gemfile
有哪些依赖呢?
上面的 Gemfile
中我们看到很多通过 cp_gem
装载的 Gem 库,其方法如下:
它是用于方便开发和调试,当 _**SKIP_UNRELEASED_VERSIONS**
_ 为 false && path
为 true
时会使用与本地的 CocoaPods 项目同级目录下的 git 仓库,否则会使用对应的项目直接通过 Gem 加载。
通过简单的目录分割和 Gemfile
管理,就实现了最基本又最直观的热插拔,对组件开发十分友好。所以你只要将多个仓库如下图方式排列,即可实现跨仓库组件开发:
组件构成和对应职责
通过上面对于 Gemfile
的简单分析,可以看出 CocoaPods 不仅仅是一个仓库那么简单,它作为一个三方库版本管理工具,对自身组件的管理和组件化也是十分讲究的。我们继续来看这份 Gemfile
中的核心开发组件:
CLAide
The CLAide gem is a simple command line parser, which provides an API that allows you to quickly create a full featured command-line interface.
CLAide 虽然是一个简单的命令行解释器,但它提供了功能齐全的命令行界面和 API。它不仅负责解析我们使用到的 Pods
命令,如:pod install
, pod update
等,还可用于封装常用的一些脚本,将其打包成简单的命令行小工具。
PS: 所谓命令行解释器就是从标准输入或者文件中读取命令并执行的程序。详见 Wiki。
cocoapods-core
The CocoaPods-Core gem provides support to work with the models of CocoaPods, for example the Podspecs or the Podfile.
CocoaPods-Core 用于 CocoaPods 中模板文件的解析,包括 Podfile
、.podspec
,以及所有的 .lock
文件中特殊的 YAML 文件。
cocoapods-downloader
The Cocoapods-downloader gem is a small library that provides downloaders for various source control types (HTTP/SVN/Git/Mercurial). It can deal with tags, commits, revisions, branches, extracting files from zips and almost anything these source control system would use.
Cocoapods-Downloader 是用于下载源码的小工具,它支持各种类型的版本管理工具,包括 HTTP / SVN / Git / Mercurial。它可以提供 tags
、commites
,revisions
,branches
以及 zips
文件的下载和解压缩操作。
Molinillo
The Molinillo gem is a generic dependency resolution algorithm, used in CocoaPods, Bundler and RubyGems.
Molinillo 是 CocoaPods 对于依赖仲裁算法的封装,它是一个具有前向检察的回溯算法。不仅在 Pods
中,Bundler
和 RubyGems
也是使用的这一套仲裁算法。
Xcodeproj
The Xcodeproj gem lets you create and modify Xcode projects from Ruby. Script boring management tasks or build Xcode-friendly libraries. Also includes support for Xcode workspaces (.xcworkspace) and configuration files (.xcconfig).
Xcodeproj 可通过 Ruby 来操作 Xcode 项目的创建和编辑等。可友好的支持 Xcode 项目的脚本管理和 libraries 构建,以及 Xcode 工作空间 (.xcworkspace) 和配置文件 .xcconfig
的管理。
cocoapods-plugins
CocoaPods plugin which shows info about available CocoaPods plugins or helps you get started developing a new plugin. Yeah, it's very meta.
cocoapods-plugins
插件管理功能,其中有 pod plugin
全套命令,支持对于 CocoaPods 插件的列表一览(list)、搜索(search)、创建(create)功能。
当然,上面还有很多组件这里就不一一介绍了。通过查看 Gemfile
可以看出 Pod 对于组件的拆分粒度是比较细微的,通过对各种组件的组合达到现在的完整版本。这些组件中,笔者的了解也十分有限,不过我们会在之后的一系列文章来逐一介绍学习。
CocoaPods 初探
接下来,结合 pod install
安装流程来展示各个组件在 Pods
工作流中的上下游关系。
命令入口
每当我们输入 pod xxx
命令时,系统会首先调用 pod
命令。所有的命令都是在 /bin
目录下存放的脚本,当然 Ruby 环境的也不例外。我们可以通过 which pod
来查看命令所在位置:
这里的显示路径不是
/usr/local/bin/pod
的原因是因为使用 RVM 进行版本控制的。
我们通过 cat
命令来查看一下这个入口脚本执行了什么
输出如下:
程序 CocoaPods 是作为 Gem 被安装的,此脚本用于唤起 CocoaPods。逻辑比较简单,就是一个单纯的命令转发。Gem.activate_bin_path
和 Gem.bin_path
用于找到 CocoaPods 的安装目录 cocoapods/bin
,最终加载该目录下的 /pod
文件:
一起来查看一下 pod
命令的输出结果:
rubyexecutablehooks 通过 bin
目录下的 pod
入口唤醒,再通过 eval 的手段调起我们需要的 CocoaPods 工程。这是 RVM 的自身行为,它利用了 [executable-hook](https://github.com/rvm/executable-hooks) 来注入 Gems 插件来定制扩展。
PS:大多数动态语言都支持
eval
这一神奇的函数。打 Lisp 开始就支持了,它通过接受一个字符串类型作为参数,将其解析成语句并混合在当前作用域内运行。详细可以参考这篇 文章。
在入口的最后部分,通过调用 Pod::Command.run(ARGV)
,实例化了一个 CLAide::Command
对象,开始我们的 CLAide 命令解析阶段。这里不对 CLAide
这个命令解析工具做过多的分析,这个是后面系列文章的内容。这里我们仅仅需要知道:
每个 CLAide 命令的执行,最终都会对应到具体 Command Class 的
run
方法。
Pod 命令对应的 run 方法实现如下:
上述所见的 Command::Install
类对应的命令为 pod install
。pod install
过程是依赖于 Podfile
文件的,所以在入口处会做检测,如果不存在 Podfile
则直接抛出 No 'Podfile' found in the project directory 的异常 警告并结束命令。
执行功能主体
在 installer
实例组装完成之后,调用其 install!
方法,这时候才进入了我们 pod install
命令的主体部分,流程如下图:
对应的实现如下:
0x1 Install 环境准备(prepare)
在 prepare
阶段会将 pod install
的环境准备完成,包括版本一致性、*目录结构*以及将 pre-install 的装载插件脚本全部取出,并执行对应的 pre_install
hook。
0x2 解决依赖冲突(resolve_dependencies)
依赖解析过程就是通过 Podfile
、Podfile.lock
以及沙盒中的 manifest
生成 Analyzer 对象。_Analyzer_ 内部会使用 _Molinillo_ (具体的是 Molinillo::DependencyGraph
图算法)解析得到一张依赖关系表。
PS:通过 Analyzer 能获取到很多依赖信息,例如 Podfile 文件的依赖分析结果,也可以从 specsbytarget 来查看各个 target 相关的 specs。
另外,需要注意的是 analyze 的过程中有一个 predownload 的阶段,即在 --verbose_ 下看到的 Fetching external sources 过程。这个 predownload_ 阶段不属于依赖下载过程,而是在当前的依赖分析阶段。
PS:该过程主要是解决当我们在通过 Git 地址引入的 Pod 仓库的情况下,系统无法从默认的 Source 拿到对应的 Spec,需要直接访问我们的 Git 地址下载仓库的 zip 包,并取出对应的
podspec
文件,从而进行对比分析。
0x3 下载依赖文件(download_dependencies)
在 create_file_accessors
中会创建沙盒目录的文件访问器,通过构造 FileAccessor
实例来解析沙盒中的各种文件。接着是最重要的 installpodsources
过程,它会调用对应 Pod 的 install!
方法进行资源下载。
先来看看 install_pod_sources
方法的实现:
在方法的开始,root_specs
方法是通过 analysis_result
拿出所有根 spec
下面再来看看 pod_installer
中的 install!
方法,主要是通过调用 cocoapods-downloader
组件,将 Pod 对应的 Source 下载到本地。实现如下:
0x4 验证 targets (validate_targets)
用来验证之前流程中的产物 (pod 所生成的 Targets) 的合法性。主要作用就是构造 TargetValidator
,并执行 validate!
方法:
验证环节在整个 Install 过程中仅占很小的一部分。因为只是验证部分,是完全解耦的。
verify_no_duplicate_framework_and_library_names
验证是否有重名的 framework
,如果有冲突会直接抛出 frameworks with conflicting names
异常。
verify_no_static_framework_transitive_dependencies
验证动态库中是否有静态链接库 (.a
或者 .framework
) 依赖,如果存在则会触发 transitive dependencies that include static binaries...
错误。假设存在以下场景:
1. 组件 A 和组件 B 同时依赖了组件 C,C 为静态库,如 Weibo_SDK
1. 组件 A 依赖组件 B,而组件 B 的 .podspec
文件中存在以下设置时,组件 B 将被判定为存在静态库依赖:
1. podspec 设置了 s.static_framework = true
1. podspec 以 s.dependency 'xxx_SDK
依赖了静态库 xxx_SDK
1. podspec 以 s.vendored_libraries = 'libxxx.a'
方式内嵌了静态库 libxxx
此时如果项目的
Podfile
设置了use_framework!
以动态链接方式打包的时,则会触发该错误。问题原因
Podfile 中不使用
use_frameworks!
时,每个 pod 是会生成相应的 .a(静态链接库)文件,然后通过 static libraries 来管理 pod 代码,在 Linked 时会包含该 pod 引用的其他的 pod 的 .a 文件。Podfile 中使用
use_frameworks!
时是会生成相应的 .framework 文件,然后通过 dynamic frameworks 的方式来管理 pod 代码,在 Linked 时会包含该 pod 引用的其他的 pod 的 .framework 文件。上述场景中虽然以 framework 的方式引用了 B 组件,然而 B 组件实际上是一个静态库,需要拷贝并链接到该 pod 中,然而 dynamic frameworks 方式并不会这么做,所以就报错了。
解决方案
1. 修改 pod 库中
podspec
,增加pod_target_xcconfig
,定义好FRAMEWORK_SEARCH_PATHS
和OTHER_LDFLAGS
两个环境变量;1. hook
verify_no_static_framework_transitive_dependencies
的方法,将其干掉!对应 issue1. 修改 pod 库中
podspec
,开启 static_framework 配置s.static_framework = true
verify_swift_pods_swift_version
确保 Swift Pod 的 Swift 版本正确配置且互相兼容的。
verify_swift_pods_have_module_dependencies
检测 Swift 库的依赖库是否支持了 module,这里的 module 主要是针对 Objective-C 库而言。
首先,Swift 是天然支持 module 系统来管理代码的,Swift Module 是构建在 LLVM Module 之上的模块系统。Swift 库在解析后会生成对应的 modulemap
和 umbrella.h
文件,这是 LLVM Module 的标配,同样 Objective-C 也是支持 LLVM Module。**当我们以 Dynamic Framework 的方式引入 Objective-C 库时,Xcode 支持配置并生成 header,而静态库 .a 需要自己编写对应的 umbrella.h
和 modulemap
**。
其次,如果你的 Swift Pod 依赖了 Objective-C 库,又希望以静态链接的方式来打包 Swift Pod 时,就需要保证 Objective-C 库启用了 modular_headers
,这样 CocoaPods 会为我们生成对应 modulemap
和 umbrella.h
来支持 LLVM Module。你可以从这个地址 - http://blog.cocoapods.org/CocoaPods-1.5.0/ 查看到更多细节。
verify_no_pods_used_with_multiple_swift_versions
检测是否所有的 Pod Target 中版本一致性问题。
用一个流程图来概括这一验证环节:
0x5 生成工程 (Integrate)
工程文件的生成是 pod install
的最后一步,他会将之前版本仲裁后的所有组件通过 Project 文件的形式组织起来,并且会对 Project 中做一些用户指定的配置。
在 install
过程中,除去依赖仲裁部分和下载部分的时间消耗,在工程文件生成也会有相对较大的时间开销。这里往往也是速度优化核心位置。
0x6 写入依赖 (write_lockfiles)
将依赖更新写入 Podfile.lock
和 Manifest.lock
0x7 结束回调(performpostinstall_action)
最后一步收尾工作,为所有插件提供 post-installation 操作以及 hook。
核心组件在 pod install
各阶段的作用如下:
总结
当我们知道 CocoaPods 在 install 的大致过程后,我们可以对其做一些修改和控制。例如知道了插件的 pre_install
和 post_install
的具体时机,我们就可以在 Podfile
中执行对应的 Ruby 脚本,达到我们的预期。同时了解 install 过程也有助于我们进行每个阶段的性能分析,以优化和提高 Install 效率。
后续,将学习 CocoaPods 中每一个组件的实现,将所有的问题在代码中找到答案。
知识点问题梳理
这里罗列了四个问题用来考察你是否已经掌握了这篇文章,如果没有建议你加入收藏再次阅读:
简单概述 CocoaPods 的核心模块?
pod
命令是如何找到并启动 CocoaPods 程序的?简述 pod install 流程?
resolve_dependencies
阶段中的pre_download
是为了解决什么问题?validate_targets
都做了哪些校验工作?
版权声明: 本文为 InfoQ 作者【Edmond】的原创文章。
原文链接:【http://xie.infoq.cn/article/bb1ccd10446697564878b8467】。文章转载请联系作者。
评论