pod install和pod update背后那点事

Cocoapods对于绝大部分的iOS开发者们应该不陌生吧~ 如果你不知道什么是CocoaPods, 请移步CocoaPods的官方Guide去学习使用指南哇~

pod installpod update应该大部分iOS以及OSX开发者最常用的两个命令了, 那么大家是否都知道pod installpod update在执行中主要做了哪些事情呢? 我们来一起探究一下呗~

初步窥探

通过CoocaPods的终端log输出, 我们也可以推测出pod install的大致行为, 如果需要更多的信息, 可以使用--verbose参数输出更多的信息, 被提炼后大致输出log如下:

  1. Updating local specs repositories - 更新本地Spec索引
  2. CocoaPods xxx is available. - 新版本试用提示
  3. Analyzing dependencies - 分析依赖
  4. Downloading dependencies - 下载依赖库
  5. Generating Pods project - 生成Pods项目
  6. Integrating client project - 整合项目

深入窥探

其实呢... 深入了解那自然最好看源码啦~ 问题是怎么跟踪去跟踪源码和学习源码... 我写这篇文章的时候CocoaPods最新的Tag是0.39.0.beta.4, 那我基于该tag来进行源码分析和追踪。

我们想要了解两个命令pod installpod 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时由下图所示的多个项目组成的。

pod install和pod update背后那点事_第1张图片
CocoaPods依赖子项目

除去主工程CocoaPods和索引库Master Repo, 还依赖了5个工程。这给源码跟踪增加了不少难度, 不过没关系, 我们不是直接阅读源代码, 只需要关注主工程CocoaPods, 依赖的库在用到或者必须跟踪的时候再去跟踪。

回归主题哇~

主工程CocoaPods源码的下的pod文件做了一些环境变量的条件控制, 主要是区分COCOAPODS_NO_BUNDLERPROFILE环境变量, 这些不是本文的关键, 直接找核心代码:

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命令背后执行了这么十件大事:

  1. 准备工作
  2. 查找依赖库
  3. 下载依赖库
  4. 决定依赖库的类型
  5. 验证没有重名的framework
  6. 验证静态库的传递依赖
  7. 验证framwoke的使用
  8. 生成工程
  9. 整合用户项目
  10. 执行install后的行为

准备工作

准备工作(prepare)主要做了以下事情:

  1. 沙盒的准备 - 一些文件以及目录的删除以及创建
  2. 确保Podfile指定的插件都已经安装(不然抛错)
  3. 迁移沙盒中部分文件(区分Pods版本迁移地址不同)
  4. 执行pre_install的Hook

查找依赖库

查找依赖库(resolve_dependencies
)主要做了以下事情:

  1. 通过HookManager添加插件源
  2. 如果config的skip_repo_update参数没有设置的时候执行Analyzer类的update_repositories方法来更新本地索引库 (这里大家其实可以看出--no-repo-update的作用了吧)
  3. 验证Build Configurations参数的有效性
  4. 准备版本兼容的遗留问题处理(0.39.0.beta.4属于空方法)
  5. 清理沙盒

下载依赖库

下载依赖库(run_podfile_pre_install_hooks)做了如下事情:

  1. 准备沙盒文件访问器
  2. 下载安装Pods依赖库源文件
  3. 执行Pods依赖库的pre install的执行钩子
  4. 根据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。

  1. 准备Pods工程(prepare_pods_project)
  2. 安装文件引用(install_file_references)
  3. 安装库(install_libraries)
  4. 为Target设置依赖(set_target_dependencies)
  5. 执行pod项目的post install的钩子(run_podfile_post_install_hooks)
  6. 执行Project类的Save方法保存配置(write_pod_project)
  7. Pods工程配置共享依赖库的Target Scheme(share_development_pod_schemes)
  8. 修改Pods工程的LockFile文件(write_lockfiles)

整合用户项目

整合项目主要是依赖UserProjectIntegrator类的integrate方法, 该方法主要是做了两件事情:

  1. 负责创建xcode的workspace, 并整合所有的target到新的workspace中.
  2. 抛出Podfile空项目依赖和xcconfig是否被原有的xcconfig所覆盖依赖相关的警告。最常见的xcconfig override警告就是这里抛出来的哦

执行install后的行为

install后的行为分为四段:

  1. unLock Pods库下的文件以便执行post install的钩子逻辑
  2. 执行Post Install的钩子逻辑
  3. 抛出签名执行收集的Spec废弃警告
  4. 重新锁定Lock Pods库下的文件防止用户误修改

到目前为止, pod install背后的十件大事都粗略的介绍完了哈。

总结

本文从pod install作为入口, 跟踪CocoaPods的实现源码, 并粗略根据tag 0.39.0.beta.4的源码将pod install背后主要执行的十个任务罗列出来。通过源码跟踪, 能够更深入的去了解Pods背后的工作, 能够更轻易排查因为Pods使用产生的问题。

另: 源码跟踪还是要自己动手更有效果哇, 因为篇幅问题, 更多细节会在后续文章中补全~~

PS: 本人水平有限, 如果有错误的地方, 请及时指出纠正, 谢谢哇!

PS: 转载请注明出处哦~~

参考文献

  1. CocoaPods官方Docs
  2. Github CocoaPods源码库

你可能感兴趣的:(pod install和pod update背后那点事)