Podfile.lock背后的那点事

目录
x
  1. 初步窥探
    1. Podfile.lock的作用
    2. 前置实验
    3. 实验主体
  • 深入窥探
    1. 调用时机
    2. 依赖Lock管理
    3. 检测依赖改动
  • 总结
    1. 参考文献
  • 大部分的iOS开发者应该都曾使用过CocoaPods去管理工程的依赖, 但是你们有没有留意一个小小的文件Podfile.lock呢?

    通常我们需要使用CocoaPods去管理依赖, 都会执行一次pod install。 在install命令的执行过程中会改变被执行Project的配置项, 同时也会生产一个Podfile.lock文件。

    那么, 这个Podfile.lock的作用究竟就是干嘛的呢?

    初步窥探

    两个问题:

    1. 什么是Podfile.lock呢?
    2. pod installpod update在工作的时候又分别对Podfile.lock文件做了什么事情呢?

    Podfile.lock的作用

    熟悉CocoaPods的开发者都知道Podfile.lock是用来团队协作的, 防止库更新影响到其他协同开发的人。官方文档描述该文件是用来追踪每一个生成Pod的版本的。原文如下:

    This file is generated after the first run of pod install, and tracks the version of each Pod that was installed.

    其实在@唐巧大神博客《用CocoaPods做iOS程序的依赖管理》中有提到update和install的区别以及Podfile.lock的关系, 原文引用如下:

    当你执行pod install之后,除了 Podfile 外,CocoaPods 还会生成一个名为Podfile.lock的文件,Podfile.lock 应该加入到版本控制里面,不应该把这个文件加入到.gitignore中。因为Podfile.lock会锁定当前各依赖库的版本,之后如果多次执行pod install 不会更改版本,要pod update才会改Podfile.lock了。这样多人协作的时候,可以防止第三方库升级时造成大家各自的第三方库版本不一致

    前置实验

    这个已经基本总结了接下来我要做的一个实验, 但是对于很多人来说, 没有做够实验, 是无法深入去理解的, 因此我把接下来的初步窥探过程给罗列了出来:

    Podfile文件写入如下执行语句:

    1
    pod 'STDebugConsole', '0.1.0'

    执行完后生成Podfile.lock文件如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    PODS:
    - STDebugConsole (0.1.0)

    DEPENDENCIES:
    - STDebugConsole (= 0.1.0)

    SPEC CHECKSUMS:
    STDebugConsole: 57c3e311e788dc1cb14f0c6aea33a931c00871cb

    COCOAPODS: 0.38.2

    修改原Podfile, 去掉后面的版本信息后重新执行pod install:

    1
    pod 'STDebugConsole'

    产生的Podfile.lock如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    PODS:
    - STDebugConsole (0.1.0)

    DEPENDENCIES:
    - STDebugConsole

    SPEC CHECKSUMS:
    STDebugConsole: 57c3e311e788dc1cb14f0c6aea33a931c00871cb

    COCOAPODS: 0.38.2

    对比可以发现DEPENDENCIES下的库没有了版本信息。

    PS: 前面的步骤只是铺垫, 为了防止STDebugConsole库升级到最新的版本做的前置步骤

    实验主体

    修改Podfile文件, 添加至少大于某个版本的信息:

    1
    pod 'STDebugConsole', '~> 0.1.0'

    重新执行pod install, 对比Podfile.lock文件如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    PODS:
    - STDebugConsole (0.1.0)

    DEPENDENCIES:
    - STDebugConsole (~> 0.1.0)

    SPEC CHECKSUMS:
    STDebugConsole: 57c3e311e788dc1cb14f0c6aea33a931c00871cb

    COCOAPODS: 0.38.2

    我们可以发现基本的PODS版本信息都没有发生过变化, 只有DEPENDNCIES信息发生变化。

    接下来我们重新执行一次pod install, 对比Podfile.lock发现并没有发生任何变化。这时候执行pod update, 可以发现STDebugConsole库的版本信息发生了变化。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    PODS:
    - STDebugConsole (0.1.1)

    DEPENDENCIES:
    - STDebugConsole (~> 0.1.0)

    SPEC CHECKSUMS:
    STDebugConsole: f07227199c0f906953e84f1d12e1fec4a9fc60d7

    COCOAPODS: 0.38.2

    通过上述几个实验, 可以得出:

    pod install只会将Podfile的信息写入到Podfile.lock, 但是不修改Pods已安装的依赖库的版本信息。pod update不但会将Podfile的信息写入到Podfile.lock文件中, 还会更新Pods已安装的依赖库的版本信息。

    深入窥探

    Install和Update的区别大家都是知道了吧,但是它的背后究竟是怎么工作的呢? 如果大家有看过我之前写的《pod install和pod update背后那点事》或者自己去研究过CocoaPods的源码, 就会发现在Pods源码下pod installlpod update两个命令均在project.rb有所体现。

    在project.rb中有两个继承Command的类, 分别叫做Update和Install, 用于分别执行pod install命令和pod update命令。Command类核心的执行方法是一个叫做run的方法, 在Update和Install类的run方法中均有调用到一个叫run_install_with_update(update)的方法。那么我们是否可以猜测, 在该方法中传递的布尔值正是决定Podfile.lock不同的地方之处呢?

    呵呵, 核心的问题来了:

    run_install_with_update方法的参数是怎么和Podfile.lock进行交互操作?

    想要知道答案? ╮(╯▽╰)╭ 没有什么好办法, 要不搜人家总结, 要么就只能去跟CocoaPods的源码了。

    PS: 本文的源码依旧基于CocoaPods Tag 0.39.0.beta.4哦~~

    跟踪Installer的update属性, 我们可以发现最终被传入了Analyzer类的update属性, 并通过一个方法重新定义返回了一个Ruby的Symbol

    1
    2
    3
    4
    5
    6
    7
    8
    9
    def update_mode
    if !update
    :none
    elsif update == true
    :all
    elsif !update[:pods].nil?
    :selected
    end
    end

    从上述代码, 我们可以看出, 如果执行Cocoapods的话, 会返回值为:all的Symbol, 因此我们使用update_mode == :all去搜索Analyzer这个类, 会发现在以下三个方法中被调用到:

    • generate_version_locking_dependencies
    • pods_to_fetch
    • dependencies_to_fetch

    具体问题具体分析, 三个方法逐个击破:

    调用时机

    • pods_to_fetchdependencies_to_fetchfetch_external_sources方法中均有被调用,
    • dependencies_to_fetch本身又只在fetch_external_sources方法中被调用
    • fetch_external_sourcesgenerate_version_locking_dependencies属于平行方法, 均在Analyzer类的核心方法analyze中被调用

    Analyzer类的核心方法analyze:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    def analyze(allow_fetches = true)
    validate_podfile!
    validate_lockfile_version!
    @result = AnalysisResult.new
    if config.integrate_targets?
    @result.target_inspections = inspect_targets_to_integrate
    else
    verify_platforms_specified!
    end
    @result.podfile_state = generate_podfile_state
    @locked_dependencies = generate_version_locking_dependencies

    store_existing_checkout_options
    fetch_external_sources if allow_fetches
    @result.specs_by_target = validate_platforms(resolve_dependencies)
    @result.specifications = generate_specifications
    @result.targets = generate_targets
    @result.sandbox_state = generate_sandbox_state
    @result
    end

    Analyzer类的核心方法是在resolve_dependencies方法中被调用的, 该方法在我之前的文章《pod install和pod update背后那点事》中有提到, 属于pod install背后执行的十大方法中的第二个方法, 在终端执行输出Analyzing dependencies的时候被调用执行。

    依赖Lock管理

    generate_version_locking_dependencies源码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    def generate_version_locking_dependencies
    if update_mode == :all || !lockfile
    LockingDependencyAnalyzer.unlocked_dependency_graph
    else
    pods_to_update = result.podfile_state.changed + result.podfile_state.deleted
    pods_to_update += update[:pods] if update_mode == :selected
    pods_to_update += podfile.dependencies.select(&:local?).map(&:name)

    LockingDependencyAnalyzer.generate_version_locking_dependencies(lockfile, pods_to_update)
    end
    end

    unlocked_dependency_graph源码:

    1
    2
    3
    def self.unlocked_dependency_graph
    Molinillo::DependencyGraph.new
    end

    generate_version_locking_dependencies源码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    def self.generate_version_locking_dependencies(lockfile, pods_to_update)
    dependency_graph = Molinillo::DependencyGraph.new

    if lockfile
    explicit_dependencies = lockfile.to_hash['DEPENDENCIES'] || []
    explicit_dependencies.each do |string|
    dependency = Dependency.new(string)
    dependency_graph.add_root_vertex(dependency.name, nil)
    end

    pods = lockfile.to_hash['PODS'] || []
    pods.each do |pod|
    add_to_dependency_graph(pod, [], dependency_graph)
    end

    pods_to_update = pods_to_update.flat_map do |u|
    root_name = Specification.root_name(u).downcase
    dependency_graph.vertices.keys.select { |n| Specification.root_name(n).downcase == root_name }
    end

    pods_to_update.each do |u|
    dependency_graph.detach_vertex_named(u)
    end
    end

    dependency_graph
    end

    通过上述源码, 可以看出update和install的区别分在LockingDependencyAnalyzer类的unlocked_dependency_graphgenerate_version_locking_dependencies方法中。

    • 如果执行update操作, 执行unlocked_dependency_graph方法, 会返回一个全新的Molinillo::DependencyGraph对象;

    • 如果执行install操作, 会执行下列操作:

      1. 生成一个全新的Molinillo::DependencyGraph的对象
      2. 根据Podfile.lock加载所有需要Lock的依赖
      3. 根据外部预先处理的pods_to_update参数剔除需要更新的依赖
      4. 返回最终剔除的需要更新后的所有需要锁定的依赖

    PS: 此处的Molinillo是CocoaPods的另外一个子项目, 专门用于处理CocoaPods的依赖关系, 引用官方介绍如下:

    A generic dependency-resolution implementation.

    检测依赖改动

    从依赖Lock管理章节可以看出, 在已有Podfile的前提下, 是需要根据pods_to_updte这个参数来进行控制调整。结合generate_version_locking_dependencies的源码分析, 咱们可以看出满足需要更新的条件是以下任一一个:

    1. Podfile的状态是changeddeleted的依赖
    2. 被预先标记需要需要的库(update参数属于数组的情况下)
    3. 在Podfile中被指定:local参数的库

    第一条和第三条都比较好理解, 但是第二条update参数属于数组的情况下, 怎么理解呢?

    首先, 这个update是个什么鬼? update是在外部方法run_install_with_update中传入的参数, 该参数可能是个数组, 可能是个BOOL值, 在这里我们回归以下Update这个Command的类的实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    class Update < Command
    # ... 省略

    def initialize(argv)
    @pods = argv.arguments! unless argv.arguments.empty?
    super
    end

    def run
    verify_podfile_exists!

    if @pods
    verify_lockfile_exists!

    #... 省略

    run_install_with_update(:pods => @pods)
    else
    UI.puts 'Update all pods'.yellow unless @pods
    run_install_with_update(true)
    end
    end

    #... 省略

    end

    从上述Update类的实现可以看出:pods这个属性是根据命令行参数读取的, 呵呵, 熟悉pod update命令的程序猿们是否已经猜出了大概, 不多说, 敲入以下命令看结果:

    1
    pod update --help

    pod update help

    原来update数组里面存的是命令行显示指定的库的集合。

    那么到目前为止问题都解决了么, 没有! 会触发更新的三个任意条件的第一条, Podfile的state又是怎么计算出来的呢? changed和deleted是依据什么去标记的呢?

    1
    2
    3
    4
    5
    def run_install_with_update(update)
    installer = Installer.new(config.sandbox, config.podfile, config.lockfile)
    installer.update = update
    installer.install!
    end

    通过run_install_with_update实现, 我们可以看出来大部分的文件都是config里预先记载的。而在config.rb的实现中有如下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    def podfile
    @podfile ||= Podfile.from_file(podfile_path) if podfile_path
    end
    attr_writer :podfile

    def lockfile
    @lockfile ||= Lockfile.from_file(lockfile_path) if lockfile_path
    end

    Podfile类和Lockfile的类的定义都无法在CocoaPods工程中搜索出来。笔者通过在CocoaPods依赖库的模糊搜索, 找出该两个库隐藏在CocoaPods下的Core依赖工程中。

    T_T ~ 好吧, 只能继续clone依赖工程了

    找到了Lockfile类和Podfile类, 下一步就是定位状态变化是在哪里的哇。在文章前半部分提到的Analyzer类的analyze方法中有个generate_podfile_state方法的调用。 看名字就像是生成状态的地方, 我们跟进去这个方法可以找出该方法的核心在于Lockfile类的detect_changes_with_podfile方法。

    Lockfile检测状态变化核心源码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    def detect_changes_with_podfile(podfile)
    result = {}
    [:added, :changed, :removed, :unchanged].each { |k| result[k] = [] }

    installed_deps = dependencies.map do |dep|
    dependencies_to_lock_pod_named(dep.root_name)
    end.flatten
    all_dep_names = (dependencies + podfile.dependencies).map(&:name).uniq
    all_dep_names.each do |name|
    installed_dep = installed_deps.find { |d| d.name == name }
    podfile_dep = podfile.dependencies.find { |d| d.name == name }

    if installed_dep.nil? then key = :added
    elsif podfile_dep.nil? then key = :removed
    elsif podfile_dep.compatible?(installed_dep) then key = :unchanged
    else key = :changed
    end
    result[key] << name
    end
    result
    end

    上述源码大概意思如下:

    • :added - Podfile中的依赖库名字在Lockfile没有被描述记载 (仅仅对比名字)
    • :removed - Lockfile中已记载的依赖库的名字没有在Podfile文件中写明 (仅仅对比名字)
    • :unchanged - 通过Dependency类的compatible不为真的时候
    • :changed - 不满足上述三个条件其他任意情况

    好了, 大致的对比方法已经了解了, 就看compatible怎么理解了!

    Dependency 类的compatible方法源码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    def compatible?(other)
    return false unless name == other.name
    return false unless head? == other.head?
    return false unless external_source == other.external_source

    other.requirement.requirements.all? do |_operator, version|
    requirement.satisfied_by? Version.new(version)
    end
    end

    Requirement 类的requirement.satisfied_by方法源码:

    1
    2
    3
    4
    5
    6
    def satisfied_by? version
    raise ArgumentError, "Need a Gem::Version: #{version.inspect}" unless
    Gem::Version === version
    # #28965: syck has a bug with unquoted '=' YAML.loading as YAML::DefaultKey
    requirements.all? { |op, rv| (OPS[op] || OPS["="]).call version, rv }
    end

    PS: OPS是一个预先定义的字符和字符对应的lambda操作映射的数组。

    通过上面的方法定义, 我们可以看出compatible方法实际上是对库名字, ext标志、 head描述和版本操作符进行比较。

    • ext、head是啥? 哈哈, 可以自己去Podfile里面对应喔~ 也可以下载代码去查找定义哇~ (如果大家没有用过这两个东西, 建议先去熟悉使用CocoaPods工具)

    detect_changes_with_podfile方法已经将Pods库响应的变化均记录在内存变量中, 供后续修改Podfile.lock, 更新依赖库等各个行为使用。

    因为继续跟踪源码文章篇幅偏长并且可阅读性也不高, 作者就打算偷个懒, 不继续跟踪源码, 如果有需要另起文章跟踪其它部分源码。

    大家可以自己根据上述的思路进行更加深入的跟踪哇~

    总结

    CocoaPods大多数人都会用, 但是并不是每一个人都理解其每一个文件的作用, Podfile.lock就是其中一个典型的例子。一开始我也无意研究Podfile.lock文件的作用, 起因是之前换工作的时候遇到一位面试的朋友对其追问而无法精确回答。

    Podfile.lock文件对团队协作非常作用, 应该添加进入源码依赖。pod installpod update都是基于Podfile.lock进行约束。install会根据一定的规则和Podfile.lock文件进行依赖库的约束下载, 同时细微更新Podfile.lock文件; update会根据另外一套规则进行约束库的下载和对Podfile.lock的写入更新。

    Podfile.lock的作用很明显, 为了相互之间协作更加通畅; 但是, Podfile.lock文件背后的工作以及作用并不是一两句话能够总结的出来, 希望大家能够理解~

    -
    另: 源码分析是具有时效性, 但是分析的方法和核心的思想不会发生太大的变化, 掌握分析的方法和理解设计的核心思想才是关键~

    PS: 个人水平有限, 如果发现错误之处, 请大家及时指出~~ 谢谢哈~

    PS: 因为本文写的特别凌乱, 如果有啥看不懂的~ 也一定要告诉我哇~~ 会持续修正文章~~

    参考文献

    1. 用CocoaPods做iOS程序的依赖管理
    2. CocoaPods源码库
    3. IBM developerWorks - 理解 Ruby Symbol,第 1 部分
    如果您觉得文章有用, 打赏一个呗~~ ( ⊙ o ⊙ )/
    本站总访问量 本站访客数人次