引子

在上文 版本管理工具及 Ruby 工具链环境 中,我们聊到如何统一管理团队小伙伴的 CocoaPods 生产环境及使用到的 Ruby 工具链。今天让我们将目光转到 CocoaPods 身上,一起来聊聊它的主要构成,以及各个组件在整个 Pods 工作流的关系。

为了整体把握 CocoaPods 这个项目,建议大家去入门一下 Ruby 这门脚本语言。另外本文基于 CocoaPods 1.9.2 版本。

CocoaPods 的核心组件

作为包管理工具,CocoaPods 随着 Apple 生态的蓬勃发展也在不断迭代和进化,并且各部分核心功能也都演化出相对独立的组件。这些功能独立的组件,均拆分出一个个独立的 Gem 包,而 CocoaPods 则是这些组件的“集大成者”。

CocoaPods 依赖总览

我们知道在 Pod 管理的项目中,Podfile 文件里描述了它所依赖的 dependencies,类似的 Gem 的依赖可以在 Gemfile 中查看。那 CocoaPods 的 Gemfile 有哪些依赖呢?

 1SKIP_UNRELEASED_VERSIONS = false
 2# ...
 3
 4source 'https://rubygems.org'
 5gemspec
 6gem 'json', :git => 'https://github.com/segiddins/json.git', :branch => 'seg-1.7.7-ruby-2.2'
 7
 8group :development do
 9  cp_gem 'claide',                'CLAide'
10  cp_gem 'cocoapods-core',        'Core', '1-9-stable'
11  cp_gem 'cocoapods-deintegrate', 'cocoapods-deintegrate'
12  cp_gem 'cocoapods-downloader',  'cocoapods-downloader'
13  cp_gem 'cocoapods-plugins',     'cocoapods-plugins'
14  cp_gem 'cocoapods-search',      'cocoapods-search'
15  cp_gem 'cocoapods-stats',       'cocoapods-stats'
16  cp_gem 'cocoapods-trunk',       'cocoapods-trunk'
17  cp_gem 'cocoapods-try',         'cocoapods-try'
18  cp_gem 'molinillo',             'Molinillo'
19  cp_gem 'nanaimo',               'Nanaimo'
20  cp_gem 'xcodeproj',             'Xcodeproj'
21   
22  gem 'cocoapods-dependencies', '~> 1.0.beta.1'
23  # ...
24  # Integration tests
25  gem 'diffy'
26  gem 'clintegracon'
27  # Code Quality
28  gem 'inch_by_inch'
29  gem 'rubocop'
30  gem 'danger'
31end
32
33group :debugging do
34  gem 'cocoapods_debug'
35
36  gem 'rb-fsevent'
37  gem 'kicker'
38  gem 'awesome_print'
39  gem 'ruby-prof', :platforms => [:ruby]
40end

上面的 Gemfile 中我们看到很多通过 cp_gem 装载的 Gem 库,其方法如下:

 1def cp_gem(name, repo_name, branch = 'master', path: false)
 2  return gem name if SKIP_UNRELEASED_VERSIONS
 3  opts = if path
 4           { :path => "../#{repo_name}" }
 5         else
 6           url = "https://github.com/CocoaPods/#{repo_name}.git"
 7           { :git => url, :branch => branch }
 8         end
 9  gem name, opts
10end

它是用于方便开发和调试,当 **SKIP_UNRELEASED_VERSIONS**false && pathtrue 时会使用与本地的 CocoaPods 项目同级目录下的 git 仓库,否则会使用对应的项目直接通过 Gem 加载。

通过简单的目录分割和 Gemfile 管理,就实现了最基本又最直观的热插拔,对组件开发十分友好。所以你只要将多个仓库如下图方式排列,即可实现跨仓库组件开发:

1$ ls -l
2lrwxr-xr-x  1 gua  staff    31 Jul 30 21:34 CocoaPods
3lrwxr-xr-x  1 gua  staff    26 Jul 31 13:27 Core 
4lrwxr-xr-x  1 gua  staff    31 Jul 31 10:14 Molinillo 
5lrwxr-xr-x  1 gua  staff    31 Aug 15 11:32 Xcodeproj 
6lrwxr-xr-x  1 gua  staff    42 Jul 31 10:14 cocoapods-downloader

组件构成和对应职责

通过上面对于 Gemfile 的简单分析,可以看出 CocoaPods 不仅仅是一个仓库那么简单,它作为一个三方库版本管理工具,对自身组件的管理和组件化也是十分讲究的。我们继续来看这份 Gemfile 中的核心开发组件:

00-Core Components

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。它可以提供 tagscommitesrevisionsbranches 以及 zips 文件的下载和解压缩操作。

Molinillo

The Molinillo gem is a generic dependency resolution algorithm, used in CocoaPods, Bundler and RubyGems.

Molinillo 是 CocoaPods 对于依赖仲裁算法的封装,它是一个具有前向检察的回溯算法。不仅在 Pods 中,BundlerRubyGems 也是使用的这一套仲裁算法。

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 来查看命令所在位置:

1$ which pod
2/Users/edmond/.rvm/gems/ruby-2.6.1/bin/pod

这里的显示路径不是 /usr/local/bin/pod 的原因是因为使用 RVM 进行版本控制的。

我们通过 cat 命令来查看一下这个入口脚本执行了什么

1$ cat /Users/edmond/.rvm/gems/ruby-2.6.1/bin/pod

输出如下:

 1#!/usr/bin/env ruby_executable_hooks
 2
 3require 'rubygems'
 4version = ">= 0.a"
 5
 6str = ARGV.first
 7if str
 8  str = str.b[/\A_(.*)_\z/, 1]
 9  if str and Gem::Version.correct?(str)
10    version = str
11    ARGV.shift
12  end
13end
14
15if Gem.respond_to?(:activate_bin_path)
16    load Gem.activate_bin_path('cocoapods', 'pod', version)
17else
18    gem "cocoapods", version
19    load Gem.bin_path("cocoapods", "pod", version)
20end

程序 CocoaPods 是作为 Gem 被安装的,此脚本用于唤起 CocoaPods。逻辑比较简单,就是一个单纯的命令转发。Gem.activate_bin_pathGem.bin_path 用于找到 CocoaPods 的安装目录 cocoapods/bin,最终加载该目录下的 /pod 文件:

 1#!/usr/bin/env ruby
 2# ... 忽略一些对于编码处理的代码
 3
 4require 'cocoapods'
 5
 6# 这里手动输出一下调用栈,来关注一下
 7puts caller
 8
 9# 如果环境配置中指定了 ruby-prof 配置文件,会对执行命令过程进行性能监控
10if profile_filename = ENV['COCOAPODS_PROFILE']
11  require 'ruby-prof'
12  # 依据配置文件类型加载不同的 reporter 解析器
13  # ...
14  File.open(profile_filename, 'w') do |io|
15    reporter.new(RubyProf.profile { Pod::Command.run(ARGV) }).print(io)
16  end
17else
18  Pod::Command.run(ARGV)
19end

一起来查看一下 pod 命令的输出结果:

1$ pod
2/Users/edmond/.rvm/gems/ruby-2.6.1/bin/pod:24:in `load'
3/Users/edmond/.rvm/gems/ruby-2.6.1/bin/pod:24:in `<main>'
4/Users/edmond/.rvm/gems/ruby-2.6.1/bin/ruby_executable_hooks:24:in `eval'
5/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 方法实现如下:

 1module Pod
 2  class Command
 3    class Install < Command
 4      # ... 
 5      def run
 6        # 判断是否存在 Podfile 文件,如果存在则进行 Podfile 的初始化
 7        verify_podfile_exists!
 8        # 从 Config 中获取一个 Instraller 实例
 9        installer = installer_for_config
10        # 默认是不执行 update
11        installer.repo_update = repo_update?(:default => false)
12        installer.update = false
13        installer.deployment = @deployment
14        # install 的真正过程
15        installer.install!
16      end
17    end
18  end
19end

上述所见的 Command::Install 类对应的命令为 pod installpod install 过程是依赖于 Podfile 文件的,所以在入口处会做检测,如果不存在 Podfile 则直接抛出 No ‘Podfile’ found in the project directory 的异常 警告并结束命令。

执行功能主体

installer 实例组装完成之后,调用其 install! 方法,这时候才进入了我们 pod install 命令的主体部分,流程如下图:

01-pod install.png

对应的实现如下:

 1def install!
 2    prepare
 3    resolve_dependencies
 4    download_dependencies
 5    validate_targets
 6    if installation_options.skip_pods_project_generation?
 7        show_skip_pods_project_generation_message
 8    else
 9        integrate
10    end
11    write_lockfiles
12    perform_post_install_actions
13end
14
15def integrate
16    generate_pods_project
17    if installation_options.integrate_targets?
18        integrate_user_project
19    else
20        UI.section 'Skipping User Project Integration'
21    end
22end

0x1 Install 环境准备(prepare)

 1def prepare
 2  # 如果检测出当前目录是 Pods,直接 raise 终止
 3  if Dir.pwd.start_with?(sandbox.root.to_path)
 4    message = 'Command should be run from a directory outside Pods directory.'
 5    message << "\n\n\tCurrent directory is #{UI.path(Pathname.pwd)}\n"
 6    raise Informative, message
 7  end
 8  UI.message 'Preparing' do
 9    # 如果 lock 文件的 CocoaPods 主版本和当前版本不同,将以新版本的配置对 xcodeproj 工程文件进行更新
10    deintegrate_if_different_major_version
11    # 对 sandbox(Pods) 目录建立子目录结构
12    sandbox.prepare
13    # 检测 PluginManager 是否有 pre-install 的 plugin
14    ensure_plugins_are_installed!
15    # 执行插件中 pre-install 的所有 hooks 方法
16    run_plugins_pre_install_hooks
17  end
18end

prepare 阶段会将 pod install 的环境准备完成,包括版本一致性目录结构以及将 pre-install 的装载插件脚本全部取出,并执行对应的 pre_install hook。

0x2 解决依赖冲突(resolve_dependencies)

 1def resolve_dependencies
 2    # 获取 Sources
 3    plugin_sources = run_source_provider_hooks
 4    # 创建一个 Analyzer
 5    analyzer = create_analyzer(plugin_sources)
 6
 7    # 如果带有 repo_update **标记**
 8    UI.section 'Updating local specs repositories' do
 9        # 执行 Analyzer 的更新 Repo 操作
10        analyzer.update_repositories
11    end if repo_update?
12
13    UI.section 'Analyzing dependencies' do
14        # 从 analyzer 取出最新的分析结果,@analysis_result,@aggregate_targets,@pod_targets
15        analyze(analyzer)
16        # 拼写错误降级识别,白名单过滤
17        validate_build_configurations
18    end
19
20    # 如果 deployment? 为 true,会验证 podfile & lockfile 是否需要更新
21    UI.section 'Verifying no changes' do
22        verify_no_podfile_changes!
23        verify_no_lockfile_changes!
24    end if deployment?
25 
26    analyzer
27end

依赖解析过程就是通过 PodfilePodfile.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 文件,从而进行对比分析。

0x3 下载依赖文件(download_dependencies)

 1def download_dependencies
 2  UI.section 'Downloading dependencies' do
 3    # 初始化 sandbox 文件访问器
 4    create_file_accessors
 5    # 构造 Pod Source Installer
 6    install_pod_sources
 7    # 执行 podfile 定义的 pre install 的 hooks
 8    run_podfile_pre_install_hooks
 9    # 根据配置清理 pod sources 信息,主要是清理无用 platform 相关内容
10    clean_pod_sources
11  end
12end

create_file_accessors 中会创建沙盒目录的文件访问器,通过构造 FileAccessor 实例来解析沙盒中的各种文件。接着是最重要的 install_pod_sources 过程,它会调用对应 Pod 的 install! 方法进行资源下载。

先来看看 install_pod_sources 方法的实现:

 1def install_pod_sources
 2 @installed_specs = []
 3 # install 的 Pod 只需要这两种状态,added 和 changed 状态的并集
 4 pods_to_install = sandbox_state.added | sandbox_state.changed
 5 title_options = { :verbose_prefix => '-> '.green }
 6 puts "root_specs"
 7 root_specs.each do |item|
 8   puts item
 9 end
10 # 将 Podfile 解析后排序处理
11 root_specs.sort_by(&:name).each do |spec|
12   # 如果是 added 或 changed 状态的 Pod
13   if pods_to_install.include?(spec.name)
14     # 如果是 changed 状态并且 manifest 已经有记录
15     if sandbox_state.changed.include?(spec.name) && sandbox.manifest
16       # 版本更新
17       current_version = spec.version
18       # 被更新版本记录
19       previous_version = sandbox.manifest.version(spec.name)
20       # 变动记录
21       has_changed_version = current_version != previous_version
22       # 找到第一个包含 spec.name 的 Pod,获取对应的 Repo,其实就是 find 方法
23       current_repo = analysis_result.specs_by_source.detect { |key, values| break key if values.map(&:name).include?(spec.name) }
24       # 获取当前仓库
25       current_repo &&= current_repo.url || current_repo.name
26       # 获取之前该仓库的信息
27       previous_spec_repo = sandbox.manifest.spec_repo(spec.name)
28       # 是否仓库有变动
29       has_changed_repo = !previous_spec_repo.nil? && current_repo && (current_repo != previous_spec_repo)
30
31       # 通过 title 输出上面的详细变更信息
32       title = ...
33     else
34       # 非 changed 状态,展示 Installing 这个是经常见的 log
35       title = "Installing #{spec}"
36     end
37     UI.titled_section(title.green, title_options) do
38       # 通过 name 拿到对应的 installer,记录到 @pod_installers 中
39       install_source_of_pod(spec.name)
40     end
41   else
42     # 如果没有 changed 情况,直接展示 Using,也是经常见到的 log
43     UI.titled_section("Using #{spec}", title_options) do
44       # # 通过 sandbox, specs 的 platform 信息生成 Installer 实例,记录到 @pod_installers 中
45       create_pod_installer(spec.name)
46     end
47   end
48 end
49end
50
51# 通过缓存返回 PodSourceInstaller 实例
52def create_pod_installer(pod_name)
53    specs_by_platform = specs_for_pod(pod_name)
54 
55    # 当通过 pod_name 无法找到对应的 pod_target 或 platform 配置,主动抛出错误信息
56    if specs_by_platform.empty?
57        requiring_targets = pod_targets.select { |pt| pt.recursive_dependent_targets.any? { |dt| dt.pod_name == pod_name } }
58        # message = "..."
59        raise StandardError, message
60    end
61    # 通过 sandbox, specs 的 platform 信息生成 Installer 实例
62    pod_installer = PodSourceInstaller.new(sandbox, podfile, specs_by_platform, :can_cache => installation_options.clean?)
63    pod_installers << pod_installer
64    pod_installer
65end
66
67# 如果 resolver 声明一个 Pod 已经安装或者已经存在,将会将其删除并重新安装。如果不存在则直接安装。
68def install_source_of_pod(pod_name)
69  pod_installer = create_pod_installer(pod_name)
70  pod_installer.install!
71  @installed_specs.concat(pod_installer.specs_by_platform.values.flatten.uniq)
72end

在方法的开始,root_specs 方法是通过 analysis_result 拿出所有根 spec

1def root_specs
2  analysis_result.specifications.map(&:root).uniq
3end

下面再来看看 pod_installer 中的 install! 方法,主要是通过调用 cocoapods-downloader 组件,将 Pod 对应的 Source 下载到本地。实现如下:

1def install!
2    download_source unless predownloaded? || local?
3    PodSourcePreparer.new(root_spec, root).prepare! if local?
4    sandbox.remove_local_podspec(name) unless predownloaded? || local? || external?
5end

0x4 验证 targets (validate_targets)

用来验证之前流程中的产物 (pod 所生成的 Targets) 的合法性。主要作用就是构造 TargetValidator,并执行 validate! 方法:

 1def validate_targets
 2    validator = Xcode::TargetValidator.new(aggregate_targets, pod_targets, installation_options)
 3    validator.validate!
 4end
 5
 6def validate!
 7    verify_no_duplicate_framework_and_library_names
 8    verify_no_static_framework_transitive_dependencies
 9    verify_swift_pods_swift_version
10    verify_swift_pods_have_module_dependencies
11    verify_no_multiple_project_names if installation_options.generate_multiple_pod_projects?
12end

验证环节在整个 Install 过程中仅占很小的一部分。因为只是验证部分,是完全解耦的。

  1. verify_no_duplicate_framework_and_library_names 验证是否有重名的 framework,如果有冲突会直接抛出 frameworks with conflicting names 异常。
  2. verify_no_static_framework_transitive_dependencies 验证动态库中是否有静态链接库 (.a 或者 .framework) 依赖,如果存在则会触发 transitive dependencies that include static binaries... 错误。假设存在以下场景:
    1. 组件 A 和组件 B 同时依赖了组件 C,C 为静态库,如 Weibo_SDK
    2. 组件 A 依赖组件 B,而组件 B 的 .podspec 文件中存在以下设置时,组件 B 将被判定为存在静态库依赖:
      1. podspec 设置了 s.static_framework = true
      2. podspec 以 s.dependency 'xxx_SDK 依赖了静态库 xxx_SDK
      3. 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_PATHSOTHER_LDFLAGS 两个环境变量;
  2. hook verify_no_static_framework_transitive_dependencies 的方法,将其干掉!对应 issue
  3. 修改 pod 库中 podspec,开启 static_framework 配置 s.static_framework = true
  1. verify_swift_pods_swift_version 确保 Swift Pod 的 Swift 版本正确配置且互相兼容的。
  2. verify_swift_pods_have_module_dependencies 检测 Swift 库的依赖库是否支持了 module,这里的 module 主要是针对 Objective-C 库而言。 首先,Swift 是天然支持 module 系统来管理代码的,Swift Module 是构建在 LLVM Module 之上的模块系统。Swift 库在解析后会生成对应的 modulemapumbrella.h 文件,这是 LLVM Module 的标配,同样 Objective-C 也是支持 LLVM Module。当我们以 Dynamic Framework 的方式引入 Objective-C 库时,Xcode 支持配置并生成 header,而静态库 .a 需要自己编写对应的 umbrella.hmodulemap。 其次,如果你的 Swift Pod 依赖了 Objective-C 库,又希望以静态链接的方式来打包 Swift Pod 时,就需要保证 Objective-C 库启用了 modular_headers,这样 CocoaPods 会为我们生成对应 modulemapumbrella.h 来支持 LLVM Module。你可以从这个地址 - http://blog.cocoapods.org/CocoaPods-1.5.0/ 查看到更多细节。
  3. verify_no_pods_used_with_multiple_swift_versions 检测是否所有的 Pod Target 中版本一致性问题。

用一个流程图来概括这一验证环节: validate targets

0x5 生成工程 (Integrate)

工程文件的生成是 pod install 的最后一步,他会将之前版本仲裁后的所有组件通过 Project 文件的形式组织起来,并且会对 Project 中做一些用户指定的配置。

 1def integrate
 2    generate_pods_project
 3    if installation_options.integrate_targets?
 4        # 集成用户配置,读取依赖项,使用 xcconfig 来配置
 5        integrate_user_project
 6    else
 7        UI.section 'Skipping User Project Integration'
 8    end
 9end
10
11def generate_pods_project
12    # 创建 stage sanbox 用于保存安装前的沙盒状态,以支持增量编译的对比
13    stage_sandbox(sandbox, pod_targets)
14    # 检查是否支持增量编译,如果支持将返回 cache result
15    cache_analysis_result = analyze_project_cache
16    # 需要重新生成的 target
17    pod_targets_to_generate = cache_analysis_result.pod_targets_to_generate
18    # 需要重新生成的 aggregate target
19    aggregate_targets_to_generate = cache_analysis_result.aggregate_targets_to_generate
20
21    # 清理需要重新生成 target 的 header 和 pod folders
22    clean_sandbox(pod_targets_to_generate)
23    # 生成 Pod Project,组装 sandbox 中所有 Pod 的 path、build setting、源文件引用、静态库文件、资源文件等
24    create_and_save_projects(pod_targets_to_generate, aggregate_targets_to_generate,
25                                cache_analysis_result.build_configurations, cache_analysis_result.project_object_version)
26
27    # SandboxDirCleaner 用于清理增量 pod 安装中的无用 headers、target support files 目录
28    SandboxDirCleaner.new(sandbox, pod_targets, aggregate_targets).clean!
29    # 更新安装后的 cache 结果到目录 `Pods/.project_cache` 下
30    update_project_cache(cache_analysis_result, target_installation_results)
31end

install 过程中,除去依赖仲裁部分和下载部分的时间消耗,在工程文件生成也会有相对较大的时间开销。这里往往也是速度优化核心位置。

0x6 写入依赖 (write_lockfiles)

将依赖更新写入 Podfile.lockManifest.lock

0x7 结束回调(perform_post_install_action)

最后一步收尾工作,为所有插件提供 post-installation 操作以及 hook。

 1def perform_post_install_actions
 2    # 调用 HooksManager 执行每个插件的 post_install 方法 
 3    run_plugins_post_install_hooks
 4    # 打印过期 pod target 警告
 5    warn_for_deprecations
 6    # 如果 pod 配置了 script phases 脚本,会主动输出一条提示消息
 7    warn_for_installed_script_phases
 8    # 输出结束信息 `Pod installation complete!`
 9    print_post_install_message
10end

核心组件在 pod install 各阶段的作用如下:

02-pod install integrate

总结

当我们知道 CocoaPods 在 install 的大致过程后,我们可以对其做一些修改和控制。例如知道了插件的 pre_installpost_install 的具体时机,我们就可以在 Podfile 中执行对应的 Ruby 脚本,达到我们的预期。同时了解 install 过程也有助于我们进行每个阶段的性能分析,以优化和提高 Install 效率。

后续,将学习 CocoaPods 中每一个组件的实现,将所有的问题在代码中找到答案。

知识点问题梳理

这里罗列了四个问题用来考察你是否已经掌握了这篇文章,如果没有建议你加入收藏再次阅读:

  1. 简单概述 CocoaPods 的核心模块?
  2. pod 命令是如何找到并启动 CocoaPods 程序的?
  3. 简述 pod install 流程?
  4. resolve_dependencies 阶段中的 pre_download 是为了解决什么问题?
  5. validate_targets 都做了哪些校验工作?