CocoaPods 历险记这个专题是 Edmond 和 冬瓜 是对于 iOS / macOS 工程中版本管理工具 CocoaPods 的实现细节、原理、源码、实践与经验的分享记录,旨在帮助你们可以更加了解这个依赖管理工具,而不只局限于pod install
和pod update
。
CocoaPods
做为业界标准,各位 iOS 开发同窗应该都不陌生。不过不少同窗对 CocoaPods
的使用基本停留在 pod install
和 pod update
上。一旦项目组件化,各业务线逻辑拆分到独立的 Pod
中后,光了解几个简单 Pod
命令是没法知足需求的,同时还面临开发环境的一致性,Pod
命令执行中的各类异常错误,都要求咱们对其有更深层的认知和 🤔。html
关于 CocoaPods
深刻的文章有不少,推荐 ObjC China 的这篇,深刻理解 CocoaPods,而本文但愿从依赖管理工具的角度来谈谈 CocoaPods
的管理理念。node
Version control systems are a category of software tools that help a software team manage changes to source code over time. Version control software keeps track of every modification to the code in a special kind of database.
软件工程中,版本控制系统是敏捷开发的重要一环,为后续的持续集成提供了保障。Source Code Manager
(SCM) 源码管理就属于 VCS 的范围之中,熟知的工具备如 Git
。而 CocoaPods
这种针对各类语言所提供的 Package Manger (PM)
也能够看做是 SCM 的一种。git
而像 Git
或 SVN
是针对项目的单个文件的进行版本控制,而 PM 则是以每一个独立的 Package 做为最小的管理单元。包管理工具都是结合 SCM
来完成管理工做,对于被 PM 接管的依赖库的文件,一般会在 Git
的 .ignore
文件中选择忽略它们。github
例如:在 Node
项目中通常会把 node_modules
目录下的文件 ignore 掉,在 iOS / macOS 项目则是 Pods
。web
Git submodules allow you to keep a git repository as a subdirectory of another git repository. Git submodules are simply a reference to another repository at a particular snapshot in time. Git submodules enable a Git repository to incorporate and track version history of external code.
Git Submodules
能够算是 PM 的“青春版”,它将单独的 git 仓库以子目录的形式嵌入在工做目录中。它不具有 PM 工具所特有的语义化版本管理、没法处理依赖共享与冲突等。只能保存每一个依赖仓库的文件状态。shell
Git
在提交更新时,会对全部文件制做一个快照并将其存在数据库中。Git 管理的文件存在 3 种状态:数据库
index area
),存在 .git/index
目录下,保存的是执行 git add
相关命令后从工做目录添加的文件。.git/
目录下,到这个状态的文件改动算是入库成功,基本不会丢失了。Git submodule 是依赖 .gitmodules
文件来记录子模块的。npm
[submodule "ReactNative"] path = ReactNative url = https://github.com/facebook/ReactNative.git
.gitmodules
仅记录了 path 和 url 以及模块名称的基本信息, 可是咱们还须要记录每一个 Submodule Repo 的 commit 信息,而这 commit 信息是记录在 .git/modules
目录下。同时被添加到 .gitmodules
中的 path 也会被 git 直接 ignore 掉。json
做为 Git Submodule 的强化版,PM 基本都具有了语义化的版本检查能力,依赖递归查找,依赖冲突解决,以及针对具体依赖的构建能力和二进制包等。简单对好比下:swift
Key File | Git submodule | CocoaPods | SPM | npm |
---|---|---|---|---|
描述文件 | .gitmodules | Podfile | Package.swift | Package.json |
锁存文件 | .git/modules | Podfile.lock | Package.resolved | package-lock.json |
从 👆 可见,PM 工具基本围绕这个两个文件来现实包管理:
除了这两个文件以外,中心化的 PM 通常会提供依赖包的托管服务,好比 npm 提供的 npmjs.com 能够集中查找和下载 npm 包。若是是去中心化的 PM 好比 iOS
的 Carthage
和 SPM
就只能经过 Git 仓库的地址了。
CocoaPods
是开发 iOS/macOS 应用程序的一个第三方库的依赖管理工具。 利用 CocoaPods
,能够定义本身的依赖关系(简称 Pods
),以及在整个开发环境中对第三方库的版本管理很是方便。
下面咱们以 CocoaPods
为例。
Podfile
Podfile
是一个文件,以 DSL(其实直接用了 Ruby 的语法)来描述依赖关系,用于定义项目所须要使用的第三方库。该文件支持高度定制,你能够根据我的喜爱对其作出定制。更多相关信息,请查阅 Podfile 指南。
Podfile.lock
这是 CocoaPods
建立的最重要的文件之一。它记录了须要被安装的 Pod 的每一个已安装的版本。若是你想知道已安装的 Pod
是哪一个版本,能够查看这个文件。推荐将 Podfile.lock
文件加入到版本控制中,这有助于整个团队的一致性。
Manifest.lock
这是每次运行 pod install
命令时建立的 Podfile.lock
文件的副本。若是你碰见过这样的错误 沙盒文件与 Podfile.lock
文件不一样步 (The sandbox is not in sync with the Podfile.lock
),这是由于 Manifest.lock
文件和 Podfile.lock
文件不一致所引发。因为 Pods
所在的目录并不总在版本控制之下,这样能够保证开发者运行 App 以前都能更新他们的 Pods
,不然 App 可能会 crash,或者在一些不太明显的地方编译失败。
Ultimately, the goal is to improve discoverability of, and engagement in, third party open-source libraries, by creating a more centralized ecosystem.
做为包管理工具,CocoaPods
的目标是为咱们提供一个更加集中的生态系统,来提升依赖库的可发现性和参与度。本质上是为了提供更好的检索和查询功能,惋惜成为了它的问题之一。由于 CocoaPods
经过官方的 Spec 仓库来管理这些注册的依赖库。随着不断新增的依赖库致使 Spec 的更新和维护成为了使用者的包袱。
好在这个问题在 1.7.2 版本中已经解决了,CocoaPods
提供了 Mater Repo CDN ,能够直接 CDN 到对应的 Pod 地址而无需在经过本地的 Spec 仓库了。同时在 1.8 版本中,官方默认的 Spec 仓库已替换为 CDN,其地址为 https://cdn.cocoapods.org。
对于一部分仅接触过 CocoaPods
的同窗,其 PM 可能并不熟悉。其实 CocoaPods
的思想借鉴了其余语言的 PM 工具,例:RubyGems
, Bundler
, npm
和 Gradle
。
咱们知道 CocoaPods
是经过 Ruby 语言实现的。它自己就是一个 Gem
包。理解了 Ruby 的依赖管理有助于咱们更好的管理不一样版本的 CocoaPods
和其余 Gem
。同时可以保证团队中的全部同事的工具是在同一个版本,这也算是敏捷开发的保证吧。
RVM
& rbenv
RVM
和 rbenv
都是管理多个 Ruby 环境的工具,它们都能提供不一样版本的 Ruby 环境管理和切换。
具体哪一个更好要看我的习惯。 固然rbenv
官方是这么说的 Why rbenv 。本文后续的实验也都是是使用rbenv
进行演示。
The RubyGems software allows you to easily download, install, and use ruby software packages on your system. The software package is called a “gem” which contains a packaged Ruby application or library.
RubyGems 是 Ruby 的一个包管理工具,这里面管理着用 Ruby 编写的工具或依赖咱们称之为 Gem。
而且 RubyGems 还提供了 Ruby 组件的托管服务,能够集中式的查找和安装 library 和 apps。当咱们使用 gem install xxx
时,会经过 rubygems.org
来查询对应的 Gem Package。而 iOS 平常中的不少工具都是 Gem 提供的,例:Bundler
,fastlane
,jazzy
,CocoaPods
等。
在默认状况下 Gems 老是下载 library 的最新版本,这没法确保所安装的 library 版本符合咱们预期。所以咱们还缺一个工具。
Bundler 是管理 Gem 依赖的工具,能够隔离不一样项目中 Gem 的版本和依赖环境的差别,也是一个 Gem。
Bundler 经过读取项目中的依赖描述文件 Gemfile
,来肯定各个 Gems 的版本号或者范围,来提供了稳定的应用环境。当咱们使用 bundle install
它会生成 Gemfile.lock
将当前 librarys 使用的具体版本号写入其中。以后,他人再经过 bundle install
来安装 libaray 时则会读取 Gemfile.lock
中的 librarys、版本信息等。
Gemfile
能够说 CocoaPods
实际上是 iOS 版的 RubyGems + Bundler 组合。Bundler 依据项目中的 Gemfile
文件来管理 Gem,而 CocoaPods
经过 Podfile 来管理 Pod。
Gemfile 配置以下:
source 'https://gems.example.com' do gem 'cocoapods', '1.8.4'是管理 Gem 依赖的工具 gem 'another_gem', :git => 'https://looseyi.github.io.git', :branch => 'master' end
可见,Podfile 的 DSL 写法和 Gemfile 一模一样。那什么状况会用到 Gemfile 呢 ?
CocoaPods
每一年都会有一些重大版本的升级,前面聊到过 CocoaPods
在 install
过程当中会对项目的 .xcodeproj
文件进行修改,不一样版本其有所不一样,这些在变动均可能致使大量 conflicts
,处理很差,项目就不能正常运行了。我想你必定不肯意去修改 .xcodeproj
的冲突。
若是项目是基于 fastlane
来进行持续集成的相关工做以及 App 的打包工做等,也须要其版本管理等功能。
讲完了这些工具的分工,而后来讲说实际的运用。咱们可使用 homebrew
+ rbenv
+ RubyGems
+ Bundler
这一整套工具链来控制一个工程中 Ruby 工具的版本依赖。
如下是我认为比较可控的 Ruby 工具链分层管理图。下面咱们逐一讲述每一层的管理方式,以及实际的操做方法。
homebrew
安装 rbenv
$ brew install rbenv
安装成功后输入 rbenv
就能够看到相关提示:
$ rbenv rbenv 1.1.2 Usage: rbenv <command> [<args>] Some useful rbenv commands are: commands List all available rbenv commands local Set or show the local application-specific Ruby version global Set or show the global Ruby version shell Set or show the shell-specific Ruby version install Install a Ruby version using ruby-build uninstall Uninstall a specific Ruby version rehash Rehash rbenv shims (run this after installing executables) version Show the current Ruby version and its origin versions List installed Ruby versions which Display the full path to an executable whence List all Ruby versions that contain the given executable See `rbenv help <command>' for information on a specific command. For full documentation, see: https://github.com/rbenv/rbenv#readme
rbenv
管理 Ruby 版本使用 rbenv
来安装一个 Ruby 版本,这里我使用刚刚 release Ruby 2.7:
$ rbenv install 2.7.0
这个安装过程有些长,由于要下载 openssl
和 Ruby 的解释器,大概要 20 分钟左右。
安装成功后,咱们让其在本地环境中生效:
$ rbenv shell 2.7.0
输入上述命令后,可能会有报错。rbenv
提示咱们在.zshrc
中增长一行eval "$(rbenv init -)"
语句来对rbenv
环境进行初始化。若是报错,咱们增长并重启终端便可。
$ ruby --version ruby 2.7.0p0 (2019-12-25 revision 647ee6f091) [x86_64-darwin19] $ which ruby /Users/gua/.rbenv/shims/ruby
切换以后咱们发现 Ruby 已经切换到 rbenv
的管理版本,而且其启动 PATH
也已经变成 rbenv
管理下的 Ruby。而且咱们能够看一下 Ruby 捆绑的 Gem
的 PATH
:
$ which gem /Users/bytedance/.rbenv/shims/gem
对应的 Gem
也已经变成 rbenv
中的 PATH
。
Gem
依赖如此,咱们使用 rbenv
已经对 Ruby 及其 Gem
环境在版本上进行了环境隔离。咱们能够经过 gem list
命令来查询当前系统环境下全部的 Gem
依赖:
$ gem list *** LOCAL GEMS *** activesupport (4.2.11.3) ... claide (1.0.3) cocoapods (1.9.3) cocoapods-core (1.9.3) cocoapods-deintegrate (1.0.4) cocoapods-downloader (1.3.0) cocoapods-plugins (1.0.0) cocoapods-search (1.0.0) cocoapods-stats (1.1.0) cocoapods-trunk (1.5.0) cocoapods-try (1.2.0)
记住这里的 CocoaPods
版本,咱们后面项目中还会查询。
如此咱们已经完成了所有的 Ruby、Gem 环境的配置,咱们经过一张漏斗图再来梳理一下:
下面咱们来实践一下,如何使用 Bundler
来锁定项目中的 Gem
环境,从而让整个团队统一 Gem
环境中的全部 Ruby 工具版本。从而避免文件冲突和没必要要的错误。
下面是在工程中对于 Gem
环境的层级图,咱们能够在项目中增长一个 Gemfile
描述,从而锁定当前项目中的 Gem
依赖环境。
如下也会逐一讲述每一层的管理方式,以及实际的操做方法。
Bundler
环境首先咱们有一个 iOS Demo 工程 - GuaDemo
:
$ ls -al total 0 drwxr-xr-x 4 gua staff 128 Jun 10 14:47 . drwxr-xr-x@ 52 gua staff 1664 Jun 10 14:47 .. drwxr-xr-x 8 gua staff 256 Jun 10 14:47 GuaDemo drwxr-xr-x@ 5 gua staff 160 Jun 10 14:47 GuaDemo.xcodeproj
首先先来初始化一个 Bundler
环境(其实就是自动建立一个 Gemfile
文件):
$ bundle init Writing new Gemfile to /Users/Gua/GuaDemo/Gemfile
Gemfile
中声明使用的 CocoaPods
版本并安装以后咱们编辑一下这个 Gemfile
文件,加入咱们当前环境中须要使用 CocoaPods 1.5.3
这个版本,则使用 Gemfile
的 DSL 编写如下内容:
# frozen_string_literal: true source "https://rubygems.org" git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } # gem "rails" gem "cocoapods", "1.5.3"
编写以后执行一下 bundle install
:
$ bundle install Fetching gem metadata from https://gems.ruby-china.com/............ Resolving dependencies... ... Fetching cocoapods 1.5.3 Installing cocoapods 1.5.3 Bundle complete! 1 Gemfile dependency, 30 gems now installed.
发现 CocoaPods 1.5.3
这个指定版本已经安装成功,而且还保存了一份 Gemfile.lock
文件用来锁存此次的依赖结果。
CocoaPods
版本操做 iOS 工程此时咱们能够检查一下当前 Bundler
环境下的 Gem
列表:
$ bundle exec gem list *** LOCAL GEMS *** activesupport (4.2.11.3) atomos (0.1.3) bundler (2.1.4) CFPropertyList (3.0.2) claide (1.0.3) cocoapods (1.5.3) ...
发现相比于全局 Gem
列表,这个列表精简了许多,而且也只是基础 Gem
依赖和 CocoaPods
的 Gem
依赖。此时咱们使用 bundle exec pod install
来执行 Install 这个操做,就可使用 CocoaPods 1.5.3
版原本执行 Pod
操做了(固然,前提是你还须要写一个 Podfile
,你们都是 iOSer 这里就略过了)。
$ bundle exec pod install Analyzing dependencies Downloading dependencies Installing SnapKit (5.0.1) Integrating client project [!] Please close any current Xcode sessions and use `GuaDemo.xcworkspace` for this project from now on. Sending stats Pod installation complete! There is 1 dependency from the Podfile and 1 total pod installed.
能够再来看一下 Podfile.lock
文件:
cat Podfile.lock PODS: - SnapKit (5.0.1) DEPENDENCIES: - SnapKit (~> 5.0.0) SPEC REPOS: https://github.com/cocoapods/specs.git: - SnapKit SPEC CHECKSUMS: SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb PODFILE CHECKSUM: 1a4b05aaf43554bc31c90f8dac5c2dc0490203e8 COCOAPODS: 1.5.3
发现使用的 CocoaPods
的版本确实是 1.5.3
。而当咱们不使用 bundle exec
执行前缀,则会使用系统环境中的 CocoaPods
版本。如此咱们也就验证了工程中的 Gem
环境和系统中的环境能够经过 Bundler
进行隔离。
SVN
、Git
,再细分出 Git Submodule
,再到各个语言的 Package Manager
也是一直在发展的。CocoaPods
做为包管理工具控制着 iOS 项目的各类依赖库,但其自身一样遵循着严格的版本控制并不断迭代。但愿你们能够从本文中认识到版本管理的重要性。Bundler
管理工程的全流程,学习了 Bundler
基础,并学习了如何控制一个项目中的 Gem
版本信息。后续咱们将会围绕 CocoaPods
,从工具链逐渐深刻到细节,根据咱们的使用经验,逐一讲解。
这里罗列了四个问题用来考察你是否已经掌握了这篇文章,若是没有建议你加入收藏再次阅读:
PM
是如何进行依赖库的版本管理?Ruby
和 RVM/rbenv
之间的关系是什么?Gem
、Bundler
和 CocaPods
之间的关系是什么?Bundler
来管理工程中的 Gem
环境?如何锁死工程内部的 CocoaPods
版本?