本文目录
引子
本文是 Core 的最后一篇,它与另外两篇文章「Podfile 解析逻辑」和「PodSpec 文件分析」共同支撑起 CocoaPods 世界的骨架。
CocoaPods-Core 这个库之所以被命名为 Core 就是因为它包含了 Podfile -> Spec Repo -> PodSpec 这条完整的链路,将散落各地的依赖库连接起来并基于此骨架不断地完善功能。
从提供各种便利的命令行工具,到依赖库与主项目的自动集成,再到提供多样的 Xcode 编译配置、单元测试、资源管理等等,最终形成了我们所见的 CocoaPods。
今天我们就来聊聊 Spec Repo
这个 PodSpec
的聚合仓库以及它的演变与问题。
Source
作为 PodSpec
的聚合仓库,Spec Repo 记录着所有 pod
所发布的不同版本的 PodSpec
文件。该仓库对应到 Core 的数据结构为 Source
,即为今天的主角。
整个 Source
的结构比较简单,它基本是围绕着 Git 来做文章,主要是对 PodSpec
文件进行各种查找更新操作。结构如下:
# 用于检查 spec 是否符合当前 Source 要求
require 'cocoapods-core/source/acceptor'
# 记录本地 source 的集合
require 'cocoapods-core/source/aggregate'
# 用于校验 source 的错误和警告
require 'cocoapods-core/source/health_reporter'
# source 管理器
require 'cocoapods-core/source/manager'
# source 元数据
require 'cocoapods-core/source/metadata'
module Pod
class Source
# 仓库默认的 Git 分支
DEFAULT_SPECS_BRANCH = 'master'.freeze
# 记录仓库的元数据
attr_reader :metadata
# 记录仓库的本地地址
attr_reader :repo
# repo 仓库地址 ~/.cocoapods/repos/{repo_name}
def initialize(repo)
@repo = Pathname(repo).expand_path
@versions_by_name = {}
refresh_metadata
end
# 读取 Git 仓库中的 remote url 或 .git 目录
def url
@url ||= begin
remote = repo_git(%w(config --get remote.origin.url))
if !remote.empty?
remote
elsif (repo + '.git').exist?
"file://#{repo}/.git"
end
end
end
def type
git? ? 'git' : 'file system'
end
# ...
end
end
Source
还有两个子类 CDNSource 和 TrunkSource,TrunkSouce 是 CocoaPods 的默认仓库。
在版本 1.7.2 之前 Master Repo 的 URL 指向为 GitHub 的 Specs 仓库[1],这也是造成我们每次 pod install
或 pod update
慢的原因之一。
它不仅保存了近 10 年来 PodSpec 文件同时还包括 Git 记录,再加上墙的原因,每次更新都非常痛苦。
而在 1.7.2 之后 CocoaPods 的默认 Source 终于改为了 CDN 指向,同时支持按需下载,缓解了 pod
更新和磁盘占用过大问题。
Source
的依赖关系如下:
回到 Source
来看其如何初始化的,可以看到其构造函数 #initialize(repo)
将传入的 repo 地址保存后,直接调用了 #refresh_metadata
来完成元数据的加载:
def refresh_metadata
@metadata = Metadata.from_file(metadata_path)
end
def metadata_path
repo + 'CocoaPods-version.yml'
end
Metadata
Metadata 是保存在 repo 目录下,名为 CocoaPods-version.yml
的文件,用于记录该 Source 所支持的 CocoaPods 的版本以及仓库的分片规则。
autoload :Digest, 'digest/md5'
require 'active_support/hash_with_indifferent_access'
require 'active_support/core_ext/hash/indifferent_access'
module Pod
class Source
class Metadata
# 最低可支持的 CocoaPods 版本,对应字段 `min`
attr_reader :minimum_cocoapods_version
# 最高可支持的 CocoaPods 版本,对应字段 `max`
attr_reader :maximum_cocoapods_version
# 最新 CocoaPods 版本,对应字段 `last`
attr_reader :latest_cocoapods_version
# 规定截取的关键字段的前缀长度和数量
attr_reader :prefix_lengths
# 可兼容的 CocoaPods 最新版本
attr_reader :last_compatible_versions
# ...
end
end
end
这里以笔者 ???? 环境中 Master 仓库下的 CocoaPods-version.yml
文件内容为例:
---
min: 1.0.0
last: 1.10.0.beta.1
prefix_lengths:
- 1
- 1
- 1
最低支持版本为 1.0.0
,最新可用版本为 1.10.0.beta.1
,以及最后这个 prefix_lengths
为 [1, 1, 1]
的数组。那么这个 prefix_lengths 的作用是什么呢 ?
要回答这个问题,我们先来看一张 Spec Repo
的目录结构图:
再 ???? 另外一个问题,为什么 CocoaPods 生成的目录结构是这样 ?
其实在 2016 年 CocoaPods Spec 仓库下的所有文件都在同级目录,不像现在这样做了分片。这个是为了解决当时用户的吐槽:GitHub 下载慢[2],最终解决方案的结果就如你所见:将 Git 仓库进行了分片。
那么问题来了,为什么分片能够提升 GitHub 下载速度?
很重要的一点是 CocoaPods 的 Spec Repo
本质上是 Git 仓库,而 Git 在做变更管理的时候,会记录目录的变更,每个子目录都会对应一个 Git model。
而当目录中的文件数量过多的时候,Git 要找出对应的变更就变得十分困难。有兴趣的同学可以查看官方说明[3]。
另外再补充一点,在 Linux 中最经典的一句话是:一切皆文件,不仅普通的文件和目录,就连块设备、管道、socket 等,也都是统一交给文件系统管理的。
也就是说就算不用 Git 来管理 Specs 仓库,当目录下存在数以万计的文件时,如何高效查找目标文件也是需要考虑的问题。
备注:关于文件系统层次结构有兴趣的同学可以查看FHS 标准[4],以及这篇文章:「一口气搞懂「文件系统」,就靠这 25 张图了」</