2023 年做 Flutter 项目时,我连续遇到两个看起来一样的问题:gem install cocoapods 很慢,安装完成后 pod install 仍然很慢。很多教程把它们统称为“CocoaPods 换源”,然后贴一组命令,但这两个命令走的不是同一条网络链路。

原文发布于掘金。这次重新整理时,我保留了当时验证过的方案,也补上版本边界:CocoaPods、镜像站和网络环境都会变化,理解链路比记住某个镜像地址更可靠。

CocoaPods 从工具安装、Specs 元数据解析到依赖源码下载的三段网络链路

图 1:RubyGems 源、Specs 源和 podspec 中的源码地址彼此独立,换错一段不会改善另一段。

先判断到底慢在哪里

CocoaPods 是 Ruby 生态中的依赖管理工具。一次从零开始的安装,至少经过三段网络访问:

阶段 常见命令 实际访问的内容 对应配置
安装工具 gem install cocoapods CocoaPods 及 Ruby gems RubyGems source
解析依赖 pod install podspec 元数据与版本索引 Specs CDN 或 Git repo
下载产物 pod install Git 仓库、压缩包或二进制 每个 podspec 的 source

例如 pod install 已经完成版本解析,却卡在某个 GitHub 地址,此时更换 Specs 镜像没有用。Specs 只告诉 CocoaPods 去哪里下载依赖,最终产物仍可能来自另一个域名。

先用详细输出观察停点:

gem install -V cocoapods
pod install --verbose

-V--verbose 不会让网络变快,但能避免在没有进度时盲等,也能把失败定位到具体阶段。

安装 CocoaPods:处理 RubyGems 源

在当时的国内网络环境里,直接访问 RubyGems 官方源很不稳定。我使用清华 RubyGems 镜像安装工具:

# 增加镜像并移除当时不可达的官方源
gem sources --add https://mirrors.tuna.tsinghua.edu.cn/rubygems/ \
  --remove https://rubygems.org/
 
# 确认当前生效的源
gem sources -l
 
# 显示详细安装过程
sudo gem install -V cocoapods

gem 是 Ruby 的包管理器,作用类似 Node.js 的 npm 或 Python 的 pip。这里切换的只是 CocoaPods 工具及其 Ruby 依赖的下载源,不会改变后续 pod install 使用的 Specs 或源码地址。

使用系统 Ruby 时,sudo gem install 可能引入权限与版本冲突。团队环境更适合用 rbenvasdf 或 Bundler 固定 Ruby 和 CocoaPods 版本:

# Gemfile
source "https://rubygems.org"
 
gem "cocoapods", "1.12.1"
bundle install
bundle exec pod install

上面的 1.12.1 是与 2023 年原文相近的示例,不是“永远应该安装”的版本。真正应该固定的是项目已经验证过的版本,并把 Gemfile.lock 提交到仓库。

解析依赖:理解 CDN 与 Git Specs 仓库

早期 CocoaPods 会同步完整的 Specs Git 仓库。这个仓库历史很长,当时约 1.3 GB;pod repo add 没有直观克隆进度,看起来像卡死。

原文使用的是清华 Git 镜像:

cd ~/.cocoapods/repos
pod repo remove master
git clone https://mirrors.tuna.tsinghua.edu.cn/git/CocoaPods/Specs.git master

手动 git clone 的主要价值不是更换了一套神秘机制,而是能看到对象接收和检出的进度。然后在项目 Podfile 顶部声明对应源:

source "https://mirrors.tuna.tsinghua.edu.cn/git/CocoaPods/Specs.git"

但这套方案有明确成本:第一次需要同步完整仓库,后续还要维护更新。CocoaPods 1.8 之后,官方默认转向按需请求的 CDN 模型,新项目通常不必克隆完整 Specs 历史:

source "https://cdn.cocoapods.org/"

所以不要不分版本地执行 rm -rf master。先运行这些命令了解当前状态:

pod --version
pod repo list
pod install --verbose

如果项目使用 CDN 且元数据访问正常,增加完整 Git Specs 仓库反而会让首次安装更慢。只有当前网络无法稳定访问 CDN、团队已经统一维护镜像,或者私有依赖明确要求 Git Specs 时,才值得切换模型。

下载依赖:Specs 可用不代表源码可用

完成版本求解后,CocoaPods 会读取每个 podspec 的 source,它可能指向 GitHub、另一个 Git 服务或压缩包 CDN。这个阶段的典型日志是已经选定版本,然后卡在 Downloading dependencies 或某个 git clone

排查时我会直接看目标 pod 的规格:

pod spec which SomePod
pod install --verbose

确认以下信息:

  • 卡住的是 Specs 元数据,还是具体源码地址;
  • Podfile.lock 是否已经固定到某个不可达版本;
  • 团队其他机器成功是因为网络可达,还是命中了本地缓存;
  • 私有仓库凭证失败是否被误判成“下载慢”;
  • 代理或镜像是否改变了下载内容的完整性。

镜像只解决可达性,不应该改变依赖解析结果。切换后必须检查 Podfile.lock 的版本和校验变化,不能看到命令成功就直接提交。

Podfile 中多个 source 的边界

使用私有 Specs 仓库时,通常会声明多个源:

source "https://git.example.com/ios/specs.git"
source "https://cdn.cocoapods.org/"
 
platform :ios, "13.0"
 
target "Runner" do
  pod "InternalAnalytics", "~> 2.4"
  pod "Alamofire", "~> 5.8"
end

源的顺序和同名 Pod 会影响解析。私有仓库不应该复制公共 Pod 的同名规格;否则同一个 Podfile.lock 在不同缓存状态下可能得到意外来源。对于关键私有依赖,团队需要明确仓库所有权、凭证分发和可用性,而不是让每个人单独修改本机配置。

团队环境比个人换源更重要

个人电脑上临时换源能解决一次安装,不能保证 CI 和同事环境稳定。更可靠的工程做法包括:

  • 用 Bundler 固定 CocoaPods 版本;
  • 提交 Podfile.lock,CI 使用 pod install 而不是随意 pod update
  • 在 CI 中记录 Ruby、CocoaPods、Xcode 和源配置;
  • 对私有 Specs 与二进制产物建立团队级缓存和可用性监控;
  • 镜像故障时有明确回退源,而不是现场搜索一条新命令;
  • 定期验证镜像同步延迟与依赖校验,避免可用但过期。

可以把处理策略归纳成一个简单决策表:

日志停点 优先动作 不该先做什么
gem install 下载 gem 检查 RubyGems source 与 Ruby 环境 删除 CocoaPods Specs 仓库
更新 Specs 元数据 判断 CDN/Git 模型与当前源可达性 反复清空全部缓存
求解版本冲突 检查 Podfile 与 lockfile 约束 把网络问题当成版本问题
下载某个 Git/zip 检查 podspec 的 source 地址 只更换 Specs 镜像
只有 CI 失败 对比凭证、代理、版本和缓存 在个人电脑上反复重装

这次问题真正教会我的事

当时最有用的不是记住清华镜像的 URL,而是终于把 CocoaPods 的网络请求拆成了三段。只有知道命令正在安装 Ruby 工具、解析 Specs,还是下载真正的依赖,才知道该改哪个配置。

环境问题最容易被写成一串“复制即可”的命令,但镜像地址、默认源和工具行为都会变化。把版本、适用时间和回退方式一起写进项目文档,才是这类经验能够长期复用的前提。

真正解决后,我会保存一份完整诊断记录:工具版本、Ruby 来源、gem sources -lpod repo list、失败域名、最终使用的镜像和 lockfile 是否变化。下次同事遇到“pod install 很慢”,先对照这份记录判断卡在哪一段,而不是重新复制一组未知命令。

镜像配置每季度验证一次可达性与同步延迟;失效时回退到团队统一方案。这里有一个容易忽略的判断:镜像和代理可以解决可达性,但永远不能改变依赖解析结果。切换源后必须检查 Podfile.lock 的版本与校验是否变化——如果变了,说明两个源的元数据不一致,这次“换源”其实是换了一组依赖,不能直接提交。环境文档如果没有验证日期和负责人,很快就会从帮助变成新的故障源。