pod install安装流程

我们先看一下install入口

在命令行中输入which pod,找到pod命令所在的目录

➜  [/Users] ✗ open /usr/local/bin/pod
➜  [/Users] ✗ open /usr/local/bin

根据路径,我们打开pod的脚本,可以看到这个脚本是用来唤起cocoapods的,流程是利用Gem.activate_bin_path 找到 CocoaPods 的安装目录 cocoapods/bin,然后使用 Gem.bin_path 来加载该目录下的 /pod 文件,然后我们打开pod文件,看到Pod::Command.run(ARGV),最终通过pod下面的run方法来进入install。

#!/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/bin/ruby
#
# This file was generated by RubyGems.
#
# The application 'cocoapods' is installed as part of a gem, and
# this file is here to facilitate running it.
#

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

//Pod文件下面的部分代码
require 'cocoapods'

if profile_filename = ENV['COCOAPODS_PROFILE']
  require 'ruby-prof'
  reporter =
    case (profile_extname = File.extname(profile_filename))
    when '.txt'
      RubyProf::FlatPrinterWithLineNumbers
    when '.html'
      RubyProf::GraphHtmlPrinter
    when '.callgrind'
      RubyProf::CallTreePrinter
    else
      raise "Unknown profiler format indicated by extension: #{profile_extname}"
    end
  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的run函数

module Pod
  class Command
    class Install < Command
    .....
    def run
        verify_podfile_exists!
        installer = installer_for_config
        installer.repo_update = repo_update?(:default => false)
        installer.update = false
        installer.deployment = @deployment
        installer.clean_install = @clean_install
        installer.install!
      end
    end
  • 判断podfile是否存在,不存在报错"No `Podfile’ found in the project directory."
  • 从podfile和lock文件中获取参数,创建installer
def installer_for_config
      Installer.new(config.sandbox, config.podfile, config.lockfile)
   end
  • 判断spec文件是否应该被更新
  • 不请求更新pod,默认不更新
# @return [Hash, Boolean, nil] Pods that have been requested to be
    #         updated or true if all Pods should be updated.
    #         If all Pods should been updated the contents of the Lockfile are
    #         not taken into account for deciding what Pods to install.
    #
    attr_accessor :update
  • installation是否验证podfile或者lockfile有更新,默认不验证
  • installation是否忽略peoject的缓存内容,默认不忽略
  • 安装pods,开始执行pod isntall的流程
def install!
      puts 'zyb'
    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

安装过程是线性的,但是需要注意以下两点

  • 为例比main返回旧的podspec文件,在执行解析之前需要清理缓存的podspec文件
  • reslocer可能会触发歪脖的source的下载,需要递归检测外部source 的spec文件

如上所述,在installer组装完毕之后,调用install!方法,进入pod install的流程中,下面我们看看pod install的流程,看看它做了哪些操作

(1)install的准备–prepare

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

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

(2)解决依赖冲突–resolve_dependencies

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

(1)通过 Analyzer 能获取到很多依赖信息,例如 Podfile 文件的依赖分析结果,也可以从 specs_by_target 来查看各个 target 相关的 specs。

(2)analyze 的过程中有一个 pre_download 的阶段,即在 –verbose 下看到的 Fetching external sources 过程。这个 pre_download 阶段不属于依赖下载过程,而是在当前的依赖分析阶段。

def resolve_dependencies
    # 获取plugin的Sources
    plugin_sources = run_source_provider_hooks
    # 通过 Podfile、Podfile.lock 以及沙盒中的 manifest 生成 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

插曲:pod repo update:用来更新pod资源目录,也就是更新master下的资源,例如,有一个第三方库发布了一个最新的版本,如果不执行pod repo update,那么你的本地是不会知道有一个最新版本的,还会一直以你本地的资源目录为准。不执行这个命令就拿不到这个库的最新版本

我们开发时不使用这个命令时因为pod update是默认会执行一遍pod repo update 。

(3)下载依赖文件–download_dependencies

先执行install_pod_sources 过程,它会调用对应 Pod 的 install! 方法进行资源下载;然后执行 podfile 定义的 pre install 的 hooks,最后根据配置清理 pod sources 信息,主要是清理无用 platform 相关内容

def download_dependencies
      UI.section 'Downloading dependencies' do
        # 下载、安装文档并清理需要安装的Pod的源代码
        install_pod_sources
        # 运行已安装specs和 Podfile 的pre_install的钩子。
        run_podfile_pre_install_hooks
        #清理pod源
        clean_pod_sources
      end
    end

(4) 验证 targets–validate_targets

验证之前流程中的产物 (pod 所生成的 Targets) 的合法性

def validate_targets
    #构造TargetValidator
    validator = Xcode::TargetValidator.new(aggregate_targets, pod_targets, installation_options)
    #用来验证提供的聚合和pod是否正确,否则报错
    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

validate方法,用来验证提供的聚合和pod是否正确,否则报错,下面是对方法内五个函数的介绍

  • 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
  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_PATHS 和 OTHER_LDFLAGS 两个环境变量;
  2. hook verify_no_static_framework_transitive_dependencies 的方法,将其干掉!对应 issue
  3. 修改 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 库在解析后会生成对应的 modulemapumbrella.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 会为我们生成对应 modulemapumbrella.h 来支持 LLVM Module。你可以从这个地址 - http://blog.cocoapods.org/CocoaPods-1.5.0/

  • verify_no_pods_used_with_multiple_swift_versions

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

(5)写入依赖–write_lockfiles

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

def write_lockfiles
    @lockfile = generate_lockfile

    UI.message "- Writing Lockfile in #{UI.path config.lockfile_path}" do
      # No need to invoke Sandbox#update_changed_file here since this logic already handles checking if the
      # contents of the file are the same.
      @lockfile.write_to_disk(config.lockfile_path)
    end

    UI.message "- Writing Manifest in #{UI.path sandbox.manifest_path}" do
      # No need to invoke Sandbox#update_changed_file here since this logic already handles checking if the
      # contents of the file are the same.
      @lockfile.write_to_disk(sandbox.manifest_path)
    end
  end

(7)结束回调–perform_post_install_action

install的收尾工作,为所有插件提供 post-installation 操作以及 hook。

def perform_post_install_actions
    #执行已安装的specs和podfile里面的post install的钩子
    run_plugins_post_install_hooks
    # Prints a warning for any pods that are deprecated
    warn_for_deprecations
    # Prints a warning for any pods that included script phases
    warn_for_installed_script_phases
    # Prints a warning if the project is not explicitly using the git based master specs repo.
    warn_for_removing_git_master_specs_repo
    print_post_install_message
  end

参考:https://www.zhihu.com/people/tu-tu-edmondmu/posts