本文知识目录

引子

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

CocoaPods 的核心组件

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

CocoaPods 依赖总览

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

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 && pathtrue 时会使用与本地的 CocoaPods 项目同级目录下的 git 仓库,否则会使用对应的项目直接通过 Gem 加载。

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

$ 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 中的核心开发组件:

CLAide

CLAide 虽然是一个简单的命令行解释器,但它提供了功能齐全的命令行界面和 API。它不仅负责解析我们使用到的 Pods 命令,如:pod install, pod update 等,还可用于封装常用的一些脚本,将其打包成简单的命令行小工具。

cocoapods-core

CocoaPods-Core 用于 CocoaPods 中模板文件的解析,包括 Podfile.podspec,以及所有的 .lock 文件中特殊的 YAML 文件。

cocoapods-downloader

Cocoapods-Downloader 是用于下载源码的小工具,它支持各种类型的版本管理工具,包括 HTTP / SVN / Git / Mercurial。它可以提供 tagscommitesrevisionsbranches 以及 zips 文件的下载和解压缩操作。

Molinillo

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

Xcodeproj

Xcodeproj 可通过 Ruby 来操作 Xcode 项目的创建和编辑等。可友好的支持 Xcode 项目的脚本管理和 libraries 构建,以及 Xcode 工作空间 (.xcworkspace)  和配置文件 .xcconfig 的管理。

cocoapods-plugins

cocoapods-plugins 插件管理功能,其中有 pod plugin 全套命令,支持对于 CocoaPods 插件的列表一览(list)、搜索(search)、创建(create)功能。

当然,上面还有很多组件这里就不一一介绍了。通过查看 Gemfile 可以看出 Pod 对于组件的拆分粒度是比较细微的,通过对各种组件的组合达到现在的完整版本。这些组件中,笔者的了解也十分有限,不过我们会在之后的一系列文章来逐一介绍学习。

CocoaPods 初探

接下来,结合 pod install 安装流程来展示各个组件在 Pods 工作流中的上下游关系。

命令入口

每当我们输入 pod xxx 命令时,系统会首先调用 pod 命令。所有的命令都是在 /bin 目录下存放的脚本,当然 Ruby 环境的也不例外。我们可以通过 which pod 来查看命令所在位置:

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

我们通过 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_pathGem.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 插件来定制扩展。

在入口的最后部分,通过调用 Pod::Command.run(ARGV),实例化了一个 CLAide::Command 对象,开始我们的 CLAide 命令解析阶段。这里不对 CLAide 这个命令解析工具做过多的分析,这个是后面系列文章的内容。这里我们仅仅需要知道:

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 installpod 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

0x1 Install 环境准备(prepare)

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。

0x2 解决依赖冲突(resolve_dependencies)

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

依赖解析过程就是通过 PodfilePodfile.lock 以及沙盒中的 manifest 生成 Analyzer 对象。_Analyzer_ 内部会使用 Molinillo (具体的是 Molinillo::DependencyGraph 图算法)解析得到一张依赖关系表。

另外,需要注意的是 analyze 的过程中有一个 pre_download 的阶段,即在 --verbose 下看到的 Fetching external sources 过程。这个 pre_download 阶段不属于依赖下载过程,而是在当前的依赖分析阶段。

0x3 下载依赖文件(download_dependencies)

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

0x4 验证 targets (validate_targets)

用来验证之前流程中的产物 (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 过程中仅占很小的一部分。因为只是验证部分,是完全解耦的。

  1. verify_no_duplicate_framework_and_library_names

验证是否有重名的 framework,如果有冲突会直接抛出 frameworks with conflicting names 异常。

  1. 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
  1. verify_swift_pods_swift_version

确保 Swift Pod 的 Swift 版本正确配置且互相兼容的。

  1. 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/ 查看到更多细节。

  1. verify_no_pods_used_with_multiple_swift_versions

检测是否所有的 Pod Target 中版本一致性问题。

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

0x5 生成工程 (Integrate)

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

0x6 写入依赖 (write_lockfiles)

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

0x7 结束回调(perform_post_install_action)

最后一步收尾工作,为所有插件提供 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_installpost_install 的具体时机,我们就可以在 Podfile 中执行对应的 Ruby 脚本,达到我们的预期。同时了解 install 过程也有助于我们进行每个阶段的性能分析,以优化和提高 Install 效率。

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

知识点问题梳理

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

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