Cocoapods对于绝大部分的iOS开发者们应该不陌生吧~ 如果你不知道什么是CocoaPods, 请移步CocoaPods的官方Guide去学习使用指南哇~
pod install
和pod update
应该大部分iOS以及OSX开发者最常用的两个命令了, 那么大家是否都知道pod install
或pod update
在执行中主要做了哪些事情呢? 我们来一起探究一下呗~
初步窥探
通过CoocaPods的终端log输出, 我们也可以推测出pod install
的大致行为, 如果需要更多的信息, 可以使用--verbose
参数输出更多的信息, 被提炼后大致输出log如下:
-
Updating local specs repositories
- 更新本地Spec索引 -
CocoaPods xxx is available.
- 新版本试用提示 -
Analyzing dependencies
- 分析依赖 -
Downloading dependencies
- 下载依赖库 -
Generating Pods project
- 生成Pods项目 -
Integrating client project
- 整合项目
深入窥探
其实呢... 深入了解那自然最好看源码啦~ 问题是怎么跟踪去跟踪源码和学习源码... 我写这篇文章的时候CocoaPods最新的Tag是0.39.0.beta.4, 那我基于该tag来进行源码分析和追踪。
我们想要了解两个命令pod install
和pod update
, 我们可以从命令行入口进行跟踪学习。现在我们以pod install
最为学习入口进行跟踪。
我们可以用终端自带的which
命令去跟踪pod
命名源码文件所在地作为跟踪入口。执行命令可以发现pod命令原来放置在用户目录下的.rvm
隐藏目录下。
which pod
# output: /Users/[UserName]/.rvm/gems/ruby-2.2.1/bin/pod
使用文本编辑器打开pod
脚本文件, 我们可以发现如下代码:
load Gem.bin_path('cocoapods', 'pod', version)
=。= 这脚本只是负责加载一个在源码目录bin
下面的pod
文件, 那么我们继续跟踪源码bin
下面的pod
文件。不多说, 赶紧进入CocoaPods项目地址拉源代码了。
进入CocoaPods的开源项目可以发现CocoaPods时由下图所示的多个项目组成的。
除去主工程CocoaPods和索引库Master Repo, 还依赖了5个工程。这给源码跟踪增加了不少难度, 不过没关系, 我们不是直接阅读源代码, 只需要关注主工程CocoaPods, 依赖的库在用到或者必须跟踪的时候再去跟踪。
回归主题哇~
主工程CocoaPods源码的下的pod
文件做了一些环境变量的条件控制, 主要是区分COCOAPODS_NO_BUNDLER
和PROFILE
环境变量, 这些不是本文的关键, 直接找核心代码:
Pod::Command.run(ARGV)
看到这个。。又要继续去跟踪Command的类了, ARGV是前面脚本传进来的参数。Command类在lib/cocoapods/command.rb
下。
在Command类下找到初始化函数入口run方法。
class Command < CLAide::Command
# ...
def self.run(argv)
help! 'You cannot run CocoaPods as root.' if Process.uid == 0
verify_xcode_license_approved!
super(argv)
ensure
UI.print_warnings
end
# ...
end
在Command类的run
方法里, 我们可以看到主要是进行了非root用户检验以及验证xcode使用协议是否已经同意, 然后执行了父类的run方法。什么? 父类? Command的父类是CLAide下的Command类, CLAide可是CoocoaPods依赖的另外一个项目啊... 不多说了, 乖乖去拉CLAide项目的源代码。
因为前面的CocoaPods主库是基于tag 0.39.0.bete.4分析的, 那么我们要该库对应使用CLAide的Tag分支。在CocoaPods的cocoapods.gemspec中有所有CocoaPods依赖的项目的制定版本信息, 通过查看该文件我们可以定位对应的CLAide项目使用版本是0.9.1。
找到CLAide项目下的Command类的run方法:
def self.run(argv = [])
plugin_prefixes.each do |plugin_prefix|
PluginManager.load_plugins(plugin_prefix)
end
argv = ARGV.coerce(argv)
command = parse(argv)
ANSI.disabled = !command.ansi_output?
unless command.handle_root_options(argv)
command.validate!
command.run
end
rescue Object => exception
handle_exception(command, exception)
end
end
我们可以发现此处主要任务是根据插件前缀预加载插件到管理器中, 通过ARGV类去转换生产一个参数对象, 然后通过参数对象放置转换函数中转换成为一个实际Command类并执行。
我们跟踪Command类中的parse方法, 可以发现parse方法取了参数中的第一个参数去调用[find_subcommand], 即用``install`去匹配command的名字, command属性的定义中描述如下:
@return [String] The name of the command. Defaults to a snake-cased
那么接下来的任务就是寻找类名为install子类的run方法了。赶紧在CoocaPods主工程中搜索install.rb这个文件,额。。没有找到。。这个源码跟踪的就断线了, 莫非一定要用Debug的方式去跟踪?
既然直接搜名字不行, 那就试试用别的途径, 从前面的跟踪推测Install总得是个Command的子类吧, 那我们尝试用Install < Command
去搜索。Pingo!! Install类果然被我们找到了, 藏在了project.rb文件下, 原来CocoaPods的开发者们把Install和Update类都放置在了project.rb中了。不多说,继续上代码:
class Install < Command
include Project
# ...
def run
verify_podfile_exists!
run_install_with_update(false)
end
end
Install类的run方法终于进入到了前面初探的执行步骤了哇, 前面这么一大堆行为都是为了处理命令哇。
到这里我们可以猜想一下Update类的run方法是否就是在调用run_install_with_update
方法时候传入参数的不同呢? 哈哈, 其实不是, Update类的run方法还要检查是否所有的pods都被install过了, 如果没有的话会主动抛错, 检查通过的才会调用run_install_with_update
方法。
verify_podfile_exists
在这里就不做赘述, 我们直接进入run_install_with_update
方法。
def run_install_with_update(update)
installer = Installer.new(config.sandbox, config.podfile, config.lockfile)
installer.update = update
installer.install!
end
这个大家可能会有个困惑: config是什么时候加载的呢? 这个大家可以自己去发掘喔~ 不然本文就要改名为CocoaPods源码分析之一二三四了。
run_install_with_update
的核心代码是调用了Installer类的install方法, 继续贴代码:
def install!
prepare
resolve_dependencies
download_dependencies
determine_dependency_product_types
verify_no_duplicate_framework_names
verify_no_static_framework_transitive_dependencies
verify_framework_usage
generate_pods_project
integrate_user_project if config.integrate_targets?
perform_post_install_actions
end
哇塞, 我要找所有东西都几乎全列在这个方法里了, 大致的行为动作如下哈, 我会每一个都粗略的介绍一番。
背后那点事
通过签名的代码跟踪,我们可以总结出pod install
命令背后执行了这么十件大事:
- 准备工作
- 查找依赖库
- 下载依赖库
- 决定依赖库的类型
- 验证没有重名的framework
- 验证静态库的传递依赖
- 验证framwoke的使用
- 生成工程
- 整合用户项目
- 执行install后的行为
准备工作
准备工作(prepare
)主要做了以下事情:
- 沙盒的准备 - 一些文件以及目录的删除以及创建
- 确保Podfile指定的插件都已经安装(不然抛错)
- 迁移沙盒中部分文件(区分Pods版本迁移地址不同)
- 执行pre_install的Hook
查找依赖库
查找依赖库(resolve_dependencies
)主要做了以下事情:
- 通过HookManager添加插件源
- 如果config的
skip_repo_update
参数没有设置的时候执行Analyzer类的update_repositories
方法来更新本地索引库 (这里大家其实可以看出--no-repo-update
的作用了吧) - 验证Build Configurations参数的有效性
- 准备版本兼容的遗留问题处理(0.39.0.beta.4属于空方法)
- 清理沙盒
下载依赖库
下载依赖库(run_podfile_pre_install_hooks
)做了如下事情:
- 准备沙盒文件访问器
- 下载安装Pods依赖库源文件
- 执行Pods依赖库的pre install的执行钩子
- 根据Config和Installers参数清理Pods的源文件
决定依赖库的类型
决定依赖库的类型(determine_dependency_product_types
)方法的作用主要是预判断库的host_requires_frameworks
存储在pod_target
属性中给后续使用。 那么主要决定什么内容呢? 参考源码中的注释:
Determines if the dependencies need to be built as dynamic frameworks or if they can be built as static libraries by checking for the Swift source presence.
主要是判断库是否需要支持动态Framework以及是否可以被Swift使用过的静态库。
验证没有重名的framework
(verify_no_duplicate_framework_names
)主要是验证了目标工程集合和Pods库工程没有命名冲突的Framework, 重点是检查了Framework的名字是否冲突; 如果冲突会抛出frameworks with conflicting names
异常
验证静态库的传递依赖
(verify_no_static_framework_transitive_dependencies
)检查了静态库里是否包含了引用的静态库, 形成传递依赖。静态库的传递依赖如果形成会主动抛出transitive dependencies that include static binaries
异常。
验证framework的使用
(verify_framework_usage
)检查是否引用了Switf书写的framework, 并且Podfile中没有指定use framework!
。如果验证不通过, 主动抛出异常。
生成工程
(generate_pods_project
)指定了八件任务, 执行分析过程相对比较复杂, 并且大部分执行动作都涉及CocoaPods依赖的另外一个工程Xcodeproj。
- 准备Pods工程(
prepare_pods_project
) - 安装文件引用(
install_file_references
) - 安装库(
install_libraries
) - 为Target设置依赖(
set_target_dependencies
) - 执行pod项目的post install的钩子(
run_podfile_post_install_hooks
) - 执行Project类的Save方法保存配置(
write_pod_project
) - Pods工程配置共享依赖库的Target Scheme(
share_development_pod_schemes
) - 修改Pods工程的LockFile文件(
write_lockfiles
)
整合用户项目
整合项目主要是依赖UserProjectIntegrator
类的integrate方法, 该方法主要是做了两件事情:
- 负责创建xcode的workspace, 并整合所有的target到新的workspace中.
- 抛出Podfile空项目依赖和xcconfig是否被原有的xcconfig所覆盖依赖相关的警告。最常见的xcconfig override警告就是这里抛出来的哦
执行install后的行为
install后的行为分为四段:
- unLock Pods库下的文件以便执行post install的钩子逻辑
- 执行Post Install的钩子逻辑
- 抛出签名执行收集的Spec废弃警告
- 重新锁定Lock Pods库下的文件防止用户误修改
到目前为止, pod install
背后的十件大事都粗略的介绍完了哈。
总结
本文从pod install
作为入口, 跟踪CocoaPods的实现源码, 并粗略根据tag 0.39.0.beta.4
的源码将pod install
背后主要执行的十个任务罗列出来。通过源码跟踪, 能够更深入的去了解Pods背后的工作, 能够更轻易排查因为Pods使用产生的问题。
另: 源码跟踪还是要自己动手更有效果哇, 因为篇幅问题, 更多细节会在后续文章中补全~~
PS: 本人水平有限, 如果有错误的地方, 请及时指出纠正, 谢谢哇!
PS: 转载请注明出处哦~~
参考文献
- CocoaPods官方Docs
- Github CocoaPods源码库