CocoaPods历险记这个专题是 Edmond和 冬瓜共同撰写,对于 iOS / macOS 工程中版本管理工具 CocoaPods 的实现细节、原理、源码、实践与经验的分享记录,旨在帮助你们可以更加了解这个依赖管理工具,而不只局限于pod install
和pod update
。
在上文 版本管理工具及 Ruby 工具链环境 中,咱们聊到如何统一管理团队小伙伴的 CocoaPods 生产环境及使用到的 Ruby 工具链。今天让咱们将目光转到 CocoaPods 身上,一块儿来聊聊它的主要构成,以及各个组件在整个 Pods 工做流的关系。html
为了总体把握 CocoaPods 这个项目,建议你们去入门一下 Ruby 这门脚本语言。另外本文基于 CocoaPods 1.9.2 版本。
做为包管理工具,CocoaPods 随着 Apple 生态的蓬勃发展也在不断迭代和进化,而且各部分核心功能也都演化出相对独立的组件。这些功能独立的组件,均拆分出一个个独立的 Gem 包,而 CocoaPods 则是这些组件的“集大成者”。git
咱们知道在 Pod 管理的项目中,Podfile
文件里描述了它所依赖的 dependencies,相似的 Gem 的依赖能够在 Gemfile
中查看。那 CocoaPods 的 Gemfile
有哪些依赖呢?github
SKIP_UNRELEASED_VERSIONS = false # ... source 'https://rubygems.org' gemspec gem 'json', :git => 'https://github.com/segiddins/json.git', :branch => 'seg-1.7.7-ruby-2.2' group :development do cp_gem 'claide', 'CLAide' cp_gem 'cocoapods-core', 'Core', '1-9-stable' cp_gem 'cocoapods-deintegrate', 'cocoapods-deintegrate' cp_gem 'cocoapods-downloader', 'cocoapods-downloader' cp_gem 'cocoapods-plugins', 'cocoapods-plugins' cp_gem 'cocoapods-search', 'cocoapods-search' cp_gem 'cocoapods-stats', 'cocoapods-stats' cp_gem 'cocoapods-trunk', 'cocoapods-trunk' cp_gem 'cocoapods-try', 'cocoapods-try' cp_gem 'molinillo', 'Molinillo' cp_gem 'nanaimo', 'Nanaimo' cp_gem 'xcodeproj', 'Xcodeproj' gem 'cocoapods-dependencies', '~> 1.0.beta.1' # ... # Integration tests gem 'diffy' gem 'clintegracon' # Code Quality gem 'inch_by_inch' gem 'rubocop' gem 'danger' end group :debugging do gem 'cocoapods_debug' gem 'rb-fsevent' gem 'kicker' gem 'awesome_print' gem 'ruby-prof', :platforms => [:ruby] end
上面的 Gemfile
中咱们看到不少经过 cp_gem
装载的 Gem 库,其方法以下:算法
def cp_gem(name, repo_name, branch = 'master', path: false) return gem name if SKIP_UNRELEASED_VERSIONS opts = if path { :path => "../#{repo_name}" } else url = "https://github.com/CocoaPods/#{repo_name}.git" { :git => url, :branch => branch } end gem name, opts end
它是用于方便开发和调试,当 **SKIP_UNRELEASED_VERSIONS**
为 false && path
为 true
时会使用与本地的 CocoaPods 项目同级目录下的 git 仓库,不然会使用对应的项目直接经过 Gem 加载。shell
经过简单的目录分割和 Gemfile
管理,就实现了最基本又最直观的热插拔,对组件开发十分友好。因此你只要将多个仓库以下图方式排列,便可实现跨仓库组件开发:json
$ ls -l lrwxr-xr-x 1 gua staff 31 Jul 30 21:34 CocoaPods lrwxr-xr-x 1 gua staff 26 Jul 31 13:27 Core lrwxr-xr-x 1 gua staff 31 Jul 31 10:14 Molinillo lrwxr-xr-x 1 gua staff 31 Aug 15 11:32 Xcodeproj lrwxr-xr-x 1 gua staff 42 Jul 31 10:14 cocoapods-downloader
经过上面对于 Gemfile
的简单分析,能够看出 CocoaPods 不只仅是一个仓库那么简单,它做为一个三方库版本管理工具,对自身组件的管理和组件化也是十分讲究的。咱们继续来看这份 Gemfile
中的核心开发组件:swift
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
等,还可用于封装经常使用的一些脚本,将其打包成简单的命令行小工具。xcode
PS: 所谓命令行解释器就是从标准输入或者文件中读取命令并执行的程序。详见 Wiki。
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 文件。缓存
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
文件的下载和解压缩操做。ruby
The Molinillo gem is a generic dependency resolution algorithm, used in CocoaPods, Bundler and RubyGems.
Molinillo 是 CocoaPods 对于依赖仲裁算法的封装,它是一个具备前向检察的回溯算法。不只在 Pods
中,Bundler
和 RubyGems
也是使用的这一套仲裁算法。
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 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 对于组件的拆分粒度是比较细微的,经过对各类组件的组合达到如今的完整版本。这些组件中,笔者的了解也十分有限,不过咱们会在以后的一系列文章来逐一介绍学习。
接下来,结合 pod install
安装流程来展现各个组件在 Pods
工做流中的上下游关系。
每当咱们输入 pod xxx
命令时,系统会首先调用 pod
命令。全部的命令都是在 /bin
目录下存放的脚本,固然 Ruby 环境的也不例外。咱们能够经过 which pod
来查看命令所在位置:
$ which pod /Users/edmond/.rvm/gems/ruby-2.6.1/bin/pod
这里的显示路径不是
/usr/local/bin/pod
的缘由是由于使用
RVM 进行版本控制的。
咱们经过 cat
命令来查看一下这个入口脚本执行了什么
$ cat /Users/edmond/.rvm/gems/ruby-2.6.1/bin/pod
输出以下:
#!/usr/bin/env ruby_executable_hooks require 'rubygems' version = ">= 0.a" str = ARGV.first if str str = str.b[/\A_(.*)_\z/, 1] if str and Gem::Version.correct?(str) version = str ARGV.shift end end if Gem.respond_to?(:activate_bin_path) load Gem.activate_bin_path('cocoapods', 'pod', version) else gem "cocoapods", version load Gem.bin_path("cocoapods", "pod", version) end
程序 CocoaPods 是做为 Gem 被安装的,此脚本用于唤起 CocoaPods。逻辑比较简单,就是一个单纯的命令转发。Gem.activate_bin_path
和 Gem.bin_path
用于找到 CocoaPods 的安装目录 cocoapods/bin
,最终加载该目录下的 /pod
文件:
#!/usr/bin/env ruby # ... 忽略一些对于编码处理的代码 require 'cocoapods' # 这里手动输出一下调用栈,来关注一下 puts caller # 若是环境配置中指定了 ruby-prof 配置文件,会对执行命令过程进行性能监控 if profile_filename = ENV['COCOAPODS_PROFILE'] require 'ruby-prof' # 依据配置文件类型加载不一样的 reporter 解析器 # ... File.open(profile_filename, 'w') do |io| reporter.new(RubyProf.profile { Pod::Command.run(ARGV) }).print(io) end else Pod::Command.run(ARGV) end
一块儿来查看一下 pod
命令的输出结果:
$ pod /Users/edmond/.rvm/gems/ruby-2.6.1/bin/pod:24:in `load' /Users/edmond/.rvm/gems/ruby-2.6.1/bin/pod:24:in `<main>' /Users/edmond/.rvm/gems/ruby-2.6.1/bin/ruby_executable_hooks:24:in `eval' /Users/edmond/.rvm/gems/ruby-2.6.1/bin/ruby_executable_hooks:24:in `<main>'
ruby_executable_hooks 经过 bin
目录下的 pod
入口唤醒,再经过 eval 的手段调起咱们须要的 CocoaPods 工程。这是 RVM 的自身行为,它利用了 executable-hook 来注入 Gems 插件来定制扩展。
PS:大多数动态语言都支持
eval
这一神奇的函数。打 Lisp 开始就支持了,它经过接受一个字符串类型做为参数,将其解析成语句并混合在当前做用域内运行。详细能够参考这篇
文章。
在入口的最后部分,经过调用 Pod::Command.run(ARGV)
,实例化了一个 CLAide::Command
对象,开始咱们的 CLAide 命令解析阶段。这里不对 CLAide
这个命令解析工具作过多的分析,这个是后面系列文章的内容。这里咱们仅仅须要知道:
每一个 CLAide 命令的执行,最终都会对应到具体 Command Class 的
run
方法。
Pod 命令对应的 run 方法实现以下:
module Pod class Command class Install < Command # ... def run # 判断是否存在 Podfile 文件,若是存在则进行 Podfile 的初始化 verify_podfile_exists! # 从 Config 中获取一个 Instraller 实例 installer = installer_for_config # 默认是不执行 update installer.repo_update = repo_update?(:default => false) installer.update = false installer.deployment = @deployment # install 的真正过程 installer.install! end end end end
上述所见的 Command::Install
类对应的命令为 pod install
。pod install
过程是依赖于 Podfile
文件的,因此在入口处会作检测,若是不存在 Podfile
则直接抛出 No 'Podfile' found in the project directory 的异常 警告并结束命令。
在 installer
实例组装完成以后,调用其 install!
方法,这时候才进入了咱们 pod install
命令的主体部分,流程以下图:
对应的实现以下:
def install! prepare resolve_dependencies download_dependencies validate_targets if installation_options.skip_pods_project_generation? show_skip_pods_project_generation_message else integrate end write_lockfiles perform_post_install_actions end def integrate generate_pods_project if installation_options.integrate_targets? integrate_user_project else UI.section 'Skipping User Project Integration' end end
def prepare # 若是检测出当前目录是 Pods,直接 raise 终止 if Dir.pwd.start_with?(sandbox.root.to_path) message = 'Command should be run from a directory outside Pods directory.' message << "\n\n\tCurrent directory is #{UI.path(Pathname.pwd)}\n" raise Informative, message end UI.message 'Preparing' do # 若是 lock 文件的 CocoaPods 主版本和当前版本不一样,将以新版本的配置对 xcodeproj 工程文件进行更新 deintegrate_if_different_major_version # 对 sandbox(Pods) 目录创建子目录结构 sandbox.prepare # 检测 PluginManager 是否有 pre-install 的 plugin ensure_plugins_are_installed! # 执行插件中 pre-install 的全部 hooks 方法 run_plugins_pre_install_hooks end end
在 prepare
阶段会将 pod install
的环境准备完成,包括版本一致性、目录结构以及将 pre-install 的装载插件脚本所有取出,并执行对应的 pre_install
hook。
def resolve_dependencies # 获取 Sources plugin_sources = run_source_provider_hooks # 建立一个 Analyzer analyzer = create_analyzer(plugin_sources) # 若是带有 repo_update **标记** UI.section 'Updating local specs repositories' do # 执行 Analyzer 的更新 Repo 操做 analyzer.update_repositories end if repo_update? UI.section 'Analyzing dependencies' do # 从 analyzer 取出最新的分析结果,@analysis_result,@aggregate_targets,@pod_targets analyze(analyzer) # 拼写错误降级识别,白名单过滤 validate_build_configurations end # 若是 deployment? 为 true,会验证 podfile & lockfile 是否须要更新 UI.section 'Verifying no changes' do verify_no_podfile_changes! verify_no_lockfile_changes! end if deployment? analyzer end
依赖解析过程就是经过 Podfile
、Podfile.lock
以及沙盒中的 manifest
生成 Analyzer 对象。_Analyzer_ 内部会使用 Molinillo (具体的是 Molinillo::DependencyGraph
图算法)解析获得一张依赖关系表。
PS:经过 Analyzer 能获取到不少依赖信息,例如 Podfile 文件的依赖分析结果,也能够从 specs_by_target 来查看各个 target 相关的 specs。
另外,须要注意的是 analyze 的过程当中有一个 pre_download 的阶段,即在 --verbose 下看到的 Fetching external sources 过程。这个 pre_download 阶段不属于依赖下载过程,而是在当前的依赖分析阶段。
PS:该过程主要是解决当咱们在经过 Git 地址引入的 Pod 仓库的状况下,系统没法从默认的 Source 拿到对应的 Spec,须要直接访问咱们的 Git 地址下载仓库的 zip 包,并取出对应的
podspec
文件,从而进行对比分析。
def download_dependencies UI.section 'Downloading dependencies' do # 初始化 sandbox 文件访问器 create_file_accessors # 构造 Pod Source Installer install_pod_sources # 执行 podfile 定义的 pre install 的 hooks run_podfile_pre_install_hooks # 根据配置清理 pod sources 信息,主要是清理无用 platform 相关内容 clean_pod_sources end end
在 create_file_accessors
中会建立沙盒目录的文件访问器,经过构造 FileAccessor
实例来解析沙盒中的各类文件。接着是最重要的 install_pod_sources
过程,它会调用对应 Pod 的 install!
方法进行资源下载。
先来看看 install_pod_sources
方法的实现:
def install_pod_sources @installed_specs = [] # install 的 Pod 只须要这两种状态,added 和 changed 状态的并集 pods_to_install = sandbox_state.added | sandbox_state.changed title_options = { :verbose_prefix => '-> '.green } puts "root_specs" root_specs.each do |item| puts item end # 将 Podfile 解析后排序处理 root_specs.sort_by(&:name).each do |spec| # 若是是 added 或 changed 状态的 Pod if pods_to_install.include?(spec.name) # 若是是 changed 状态而且 manifest 已经有记录 if sandbox_state.changed.include?(spec.name) && sandbox.manifest # 版本更新 current_version = spec.version # 被更新版本记录 previous_version = sandbox.manifest.version(spec.name) # 变更记录 has_changed_version = current_version != previous_version # 找到第一个包含 spec.name 的 Pod,获取对应的 Repo,其实就是 find 方法 current_repo = analysis_result.specs_by_source.detect { |key, values| break key if values.map(&:name).include?(spec.name) } # 获取当前仓库 current_repo &&= current_repo.url || current_repo.name # 获取以前该仓库的信息 previous_spec_repo = sandbox.manifest.spec_repo(spec.name) # 是否仓库有变更 has_changed_repo = !previous_spec_repo.nil? && current_repo && (current_repo != previous_spec_repo) # 经过 title 输出上面的详细变动信息 title = ... else # 非 changed 状态,展现 Installing 这个是常常见的 log title = "Installing #{spec}" end UI.titled_section(title.green, title_options) do # 经过 name 拿到对应的 installer,记录到 @pod_installers 中 install_source_of_pod(spec.name) end else # 若是没有 changed 状况,直接展现 Using,也是常常见到的 log UI.titled_section("Using #{spec}", title_options) do # # 经过 sandbox, specs 的 platform 信息生成 Installer 实例,记录到 @pod_installers 中 create_pod_installer(spec.name) end end end end # 经过缓存返回 PodSourceInstaller 实例 def create_pod_installer(pod_name) specs_by_platform = specs_for_pod(pod_name) # 当经过 pod_name 没法找到对应的 pod_target 或 platform 配置,主动抛出错误信息 if specs_by_platform.empty? requiring_targets = pod_targets.select { |pt| pt.recursive_dependent_targets.any? { |dt| dt.pod_name == pod_name } } # message = "..." raise StandardError, message end # 经过 sandbox, specs 的 platform 信息生成 Installer 实例 pod_installer = PodSourceInstaller.new(sandbox, podfile, specs_by_platform, :can_cache => installation_options.clean?) pod_installers << pod_installer pod_installer end # 若是 resolver 声明一个 Pod 已经安装或者已经存在,将会将其删除并从新安装。若是不存在则直接安装。 def install_source_of_pod(pod_name) pod_installer = create_pod_installer(pod_name) pod_installer.install! @installed_specs.concat(pod_installer.specs_by_platform.values.flatten.uniq) end
在方法的开始,root_specs
方法是经过 analysis_result
拿出全部根 spec
def root_specs analysis_result.specifications.map(&:root).uniq end
下面再来看看 pod_installer
中的 install!
方法,主要是经过调用 cocoapods-downloader
组件,将 Pod 对应的 Source 下载到本地。实现以下:
def install! download_source unless predownloaded? || local? PodSourcePreparer.new(root_spec, root).prepare! if local? sandbox.remove_local_podspec(name) unless predownloaded? || local? || external? end
用来验证以前流程中的产物 (pod 所生成的 Targets) 的合法性。主要做用就是构造 TargetValidator
,并执行 validate!
方法:
def validate_targets validator = Xcode::TargetValidator.new(aggregate_targets, pod_targets, installation_options) validator.validate! end def validate! verify_no_duplicate_framework_and_library_names verify_no_static_framework_transitive_dependencies verify_swift_pods_swift_version verify_swift_pods_have_module_dependencies verify_no_multiple_project_names if installation_options.generate_multiple_pod_projects? end
验证环节在整个 Install 过程当中仅占很小的一部分。由于只是验证部分,是彻底解耦的。
验证是否有重名的 framework
,若是有冲突会直接抛出 frameworks with conflicting names
异常。
验证动态库中是否有静态连接库 (.a
或者 .framework
) 依赖,若是存在则会触发 transitive dependencies that include static binaries...
错误。假设存在如下场景:
Weibo_SDK
组件 A 依赖组件 B,而组件 B 的 .podspec
文件中存在如下设置时,组件 B 将被断定为存在静态库依赖:
s.static_framework = true
s.dependency 'xxx_SDK
依赖了静态库 xxx_SDK
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 方式并不会这么作,因此就报错了。
解决方案
- 修改 pod 库中
podspec
,增长pod_target_xcconfig
,定义好FRAMEWORK_SEARCH_PATHS
和OTHER_LDFLAGS
两个环境变量;- hook
verify_no_static_framework_transitive_dependencies
的方法,将其干掉!对应 issue- 修改 pod 库中
podspec
,开启 static_framework 配置s.static_framework = true
确保 Swift Pod 的 Swift 版本正确配置且互相兼容的。
检测 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/ 查看到更多细节。
检测是否全部的 Pod Target 中版本一致性问题。
用一个流程图来归纳这一验证环节:
工程文件的生成是 pod install
的最后一步,他会将以前版本仲裁后的全部组件经过 Project 文件的形式组织起来,而且会对 Project 中作一些用户指定的配置。
def integrate generate_pods_project if installation_options.integrate_targets? # 集成用户配置,读取依赖项,使用 xcconfig 来配置 integrate_user_project else UI.section 'Skipping User Project Integration' end end def generate_pods_project # 建立 stage sanbox 用于保存安装前的沙盒状态,以支持增量编译的对比 stage_sandbox(sandbox, pod_targets) # 检查是否支持增量编译,若是支持将返回 cache result cache_analysis_result = analyze_project_cache # 须要从新生成的 target pod_targets_to_generate = cache_analysis_result.pod_targets_to_generate # 须要从新生成的 aggregate target aggregate_targets_to_generate = cache_analysis_result.aggregate_targets_to_generate # 清理须要从新生成 target 的 header 和 pod folders clean_sandbox(pod_targets_to_generate) # 生成 Pod Project,组装 sandbox 中全部 Pod 的 path、build setting、源文件引用、静态库文件、资源文件等 create_and_save_projects(pod_targets_to_generate, aggregate_targets_to_generate, cache_analysis_result.build_configurations, cache_analysis_result.project_object_version) # SandboxDirCleaner 用于清理增量 pod 安装中的无用 headers、target support files 目录 SandboxDirCleaner.new(sandbox, pod_targets, aggregate_targets).clean! # 更新安装后的 cache 结果到目录 `Pods/.project_cache` 下 update_project_cache(cache_analysis_result, target_installation_results) end
在 install
过程当中,除去依赖仲裁部分和下载部分的时间消耗,在工程文件生成也会有相对较大的时间开销。这里每每也是速度优化核心位置。
将依赖更新写入 Podfile.lock
和 Manifest.lock
最后一步收尾工做,为全部插件提供 post-installation 操做以及 hook。
def perform_post_install_actions # 调用 HooksManager 执行每一个插件的 post_install 方法 run_plugins_post_install_hooks # 打印过时 pod target 警告 warn_for_deprecations # 若是 pod 配置了 script phases 脚本,会主动输出一条提示消息 warn_for_installed_script_phases # 输出结束信息 `Pod installation complete!` print_post_install_message end
核心组件在 pod install
各阶段的做用以下:
当咱们知道 CocoaPods 在 install 的大体过程后,咱们能够对其作一些修改和控制。例如知道了插件的 pre_install
和 post_install
的具体时机,咱们就能够在 Podfile
中执行对应的 Ruby 脚本,达到咱们的预期。同时了解 install 过程也有助于咱们进行每一个阶段的性能分析,以优化和提升 Install 效率。
后续,将学习 CocoaPods 中每个组件的实现,将全部的问题在代码中找到答案。
这里罗列了四个问题用来考察你是否已经掌握了这篇文章,若是没有建议你加入收藏再次阅读:
pod
命令是如何找到并启动 CocoaPods 程序的?resolve_dependencies
阶段中的 pre_download
是为了解决什么问题?validate_targets
都作了哪些校验工做?