CocoaPods Source 管理机制
CocoaPods-Core 主要包括三部分功能,分别是:Podfile 解析、Podspec 解析、Source 管理,前两个功能我们在之前的文章中已经分别进行了介绍,本文我们再来介绍一下最后一个功能——CocoaPods Source 管理机制。
基本原理
Source,即 podspec 源。Source 管理机制,本质上就是对 podspec 进行管理。CocoaPods 采用 集中式 的源管理方案,其基本原理 使用一个源仓库管理所有 pod 所发布的不同版本的 podspec 文件,每一个 podspec 记录了 pod 库的资源地址以及其他相关信息。
CocoaPods 在集中式管理的基础上,实现了两套具体方案:master 和 trunk。其中,master 是 CocoaPods 1.8.0 之前版本所采用的默认方案;trunk 是 CocoaPods 1.8.0 以之后版本所采用的的默认方案。
默认,CocoaPods 会把源仓库拷贝或克隆到本地用户根目录下的
~/.cocoapods/repos/
。在 ~/.cocoapods/repos/
目录下,其实我们可能看到两套方案各自对应一个目录:master
、trunk
,如下所示。
1 | ls ~/.cocoapods/repos |
下面,我们对这两种方案分别进行介绍。
Master
对于 master 方案,其源仓库(下文简称 master repo)的组织结构如下图所示。其具体方案是:
- 根据 pod 的名称计算得到 MD5 值。如:md5("AFNetworking") => a75d452377f3996bdc4b623a5df25820
- 获取 MD5 值的前 N 位,默认是 3 位。如:a75
- 对前 N
位值进行拆解,每一位对应一个目录,构成一个多级目录。如:
a/7/5
- 将 pod 及其各个版本的 podspec 文件存放在对应的目录下。
那么问题来了,为什么要使用一个多级目录结构存储 podspec 呢?
其实,在 2016 年 master repo 仓库下的所有文件都在同级目录,对目录进行分级是为了解决 Github 下载慢 的问题。
那么问题又来了,为什么对目录进行分级能加速 github 下载呢?
本质上,这与 git 的底层原理有关。在 git 对象模型中,对于目录的每次更改都会生成一个新的树对象,许多 git 操作需要遍历这棵树,内部必须通过多步增量重新创建,每一步都必须找到并解压缩。当一个目录下存在非常多的子目录的情况下,一个变更操作会生成非常大的树对象。因此,采用多级目录结构可以降低 git 对象模型的大小,从而降低 git 仓库大小,提升下载速度。
Trunk
尽管 master repo 采用多级目录的方式优化了仓库的大小,但是架不住 pod 库以及版本迭代实在太多了。于是,CocoaPods 1.7.0 支持了 CDN。
CDN 的基本思路是:在网络各处放置节点服务器,构成一个虚拟网络,CDN 系统能够实时地根据网络流量和各节点的连接、负载状况、响应时间等综合信息,将用户请求转发到里用户最近的服务节点,从而在网络链路上进行加速。
然而,CDN 并没有解决本质问题,master repo 这种全量下载的方式其实是有很大的优化空间的。于是,CocoaPods 1.8.0 支持了按需下载的方式,即 trunk 方案。
对于 trunk 方案,其源仓库(下文简称 trunk repo)的组织结构如下图所示。其具体方案是:
- 使用
.txt
文件记录一系列 pod 名称及其所有版本号。 - 同一
.txt
文件所记录的所有 pod 的名称的 MD5 值前 N 位相同,同时.txt
文件以此 N 位 MD5 值作为文件名称的标识元素。如:all_pods_version_a_7_5.txt
。 - 当请求某个 pod 的 podspec 文件时,根据 pod 名称计算得到
.txt
文件名称。如:AFNetworking ->all_pods_version_a_7_5.txt
。 - 使用对应的
.etag
文件(如果没有则不使用)请求对应的.txt
文件。.etag
文件用于表示文件是否更新。如果远端文件未更新,则使用本地已下载的.txt
文件,如果远端文件已更新,则下载最新的.txt
文件。
整体架构
下图所示为 source 管理机制的整体架构类图,主要有以下几个核心部分组成:
Manager
:source 的管理类,作为 source 管理机制的代理,外部操作由 Manager 类提供接口并进行转发。Aggregate
:source 的聚合类,管理所有 source,承接 Manager 类接口转发,并转发至各个 source,对外隐藏了内部查找逻辑。Source
:描述一个 podspec 源,master repo、trunk repo 各自对应一个 Source 实例。Metadata
:描述一个源的元信息。Specification
:描述一个 podspec 文件。Dependency
:描述一个 pod 的一个依赖,如:podsepc 文件中定义的dependency
。Set
:描述一个 pod 所对应的源信息,主要用于描述查询结果。
Manager
如下所示,为 Source::Manager
类核心部分的定义,Manager
使用 repos
目录作为初始化参数,默认是 ~/.cocoapods/repos
。
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66# source/manager.rb
module Pod
class Source
class Manager
# 包含多个 repo 的目录
attr_reader :repos_dir
def initialize(repos_dir)
@repos_dir = Pathname(repos_dir).expand_path
end
# source repos 的聚合
def aggregate
aggregate_with_repos(source_repos)
end
# source repos 的聚合
def aggregate_with_repos(repos)
sources = repos.map { |path| source_from_path(path) }
@aggregates_by_repos ||= {}
@aggregates_by_repos[repos] ||= Source::Aggregate.new(sources)
end
# 给定名称,返回对应的源
def source_with_name(name)
source = sources([name]).first
return nil unless source.repo.exist?
source
end
# 给定路径,返回对应的源
def source_from_path(path)
@sources_by_path ||= Hash.new do |hash, key|
hash[key] = case
when key.basename.to_s == Pod::TrunkSource::TRUNK_REPO_NAME
TrunkSource.new(key)
when (key + '.url').exist?
CDNSource.new(key)
else
Source.new(key)
end
end
@sources_by_path[path]
end
# 返回所有源的路径
def source_repos
return [] unless repos_dir.exist?
repos_dir.children.select(&:directory?).sort_by { |d| d.basename.to_s.downcase }
end
# 给定一组名称,返回对应的一组源
def sources(names)
dirs = names.map { |name| source_dir(name) }
dirs.map { |repo| source_from_path(repo) }
end
# 返回所有的源
def all
aggregate.sources
end
# ...
end
end
end
Manager
负责初始化 ~/.cocoapods/repos
目录下所有的 source,并将它们聚合至 Aggregate
中,具体步骤包括以下几个关键步骤:
- 执行
source_repos
方法,获取所有 source 路径 - 执行
source_from_path
方法,根据 source 路径初始化Source
对象。- 如果路径节点目录的名称为
trunk
,则实例化TrunkSource
对象。 - 如果名为
.url
文件,则实例化CDNSource
对象。 - 其他情况则实例化
Source
对象。
- 如果路径节点目录的名称为
- 聚合所有 source 对象,实例化
Aggregate
对象。
Aggregate
Manager
实现了初始化 Aggregate
相关方法,除此之外,Manager
提供了一系列查询方法,这些方法最终会转发至 Aggregate
。
如下所示,为 Aggregate
类核心部分的定义,其提供了一系列查询方法,实现 Manager
转发过来的查询方法,如:search(dependency)
方法。
1 | module Pod |
Source
Source
用于描述一个 podspec 源,其负责管理其包含的所有
podspec 文件。其核心定义如下所示:
1 | module Pod |
Source
类还提供了一系列方法以供查询 podspec,这些方法是
Manager
所提供的查询方法的最终实现,具体的方法实现不在详细介绍,有兴趣可以去阅读源码。
CDNSource
CDNSource
是 Source
的子类,其提供了一种按需加载的策略,该策略的核心逻辑由
download_and_save_with_retries_async
方法实现,如下所示:
1 | module Pod |
TrunkSource
TrunkSource
是 CDNSource
的子类,其实现非常简单,仅仅是重写了 trunk repo
的远端地址,具体如下所示:
1 | module Pod |
Metadata
Metadata
使用一个 CocoaPods-version.yml
文件记录 source 的元信息,并保存在source repo 的根目录下。CocoaPods
通过该文件实例化一个 Metadata
对象,如下所示为
Metadata
类的核心部分定义。
1 | module Pod |
Metadata
中定义的 path_fragment
方法会根据
pod name 和 version 来生成 pod 对应的索引目录:
- 根据 pod name 计算 MD5 值
- 遍历
prefix_lengths
对生成的 MD5 值迭代截取指定长度,并作为分级目录的名称
以 AFNetworking
为例,其 MD5 值如下所示: 1
2irb> Digest::MD5.hexdigest("AFNetworking")
=> "a75d452377f3996bdc4b623a5df25820"prefix_lengths
的值为
[1, 1, 1]
,那么其生成的目录名称分别是:a
、7
、5
。
Specification
Specification
用于表示 podspec 文件,其相关的内容我们在
《CocoaPods
podspec 解析原理》 中进行了介绍,这里不再进行赘述。
Dependency
Dependency
用于描述一个 pod 的依赖,我们在 Podfile
中为一个 pod 声明的 dependency,或者,在 podspec 指定的
dependency,最终都由 Dependency
进行描述。其核心定义如下所示:
1 | module Pod |
总结
本文,通过阅读 CocoaPods-Core 源码,梳理了一下 source 管理机制及其设计结构,简化的结构示意图如下所示。
此外,我们还了解了 CocoaPods 中的两种 source 管理机制:master、trunk。这两种机制基本原理相同,在实现细节上有所区别,一种是全量下载,一种是按需下载,因此在下载速度方面有着明显的区别。
后面有时间的话,我们再根据一些具体的 pod 命令来分析它们的执行逻辑,了解 CocoaPods 中更多的实现细节。