02 | 依赖管理:如何使用 CocoaPods 统一依赖库的管理?

在 iOS App 开发方面,几乎所有的 App 都需要使用到第三方依赖库。依赖库不仅能为我们提供丰富的功能,还能避免我们从头开发,在节省时间的同时也减少许多 Bug 。

但伴随着软件功能越来越丰富,依赖库数量越来越多,由此也出现了“依赖地狱”,比如依赖库循环依赖,底层依赖库版本冲突等。为了解决此类问题,于是,依赖库管理工具也就出现了。

目前流行的依赖库管理工具主要有:Git Submodules、Carthage、 Swift Package Manager 和 CocoaPods。在这里我们选择 CocoaPods。为什么呢?原因有三:

CocoaPods 非常成熟,十分稳定,并且简单易用,学习成本低,效果明显;

CocoaPods 会自动整合 Xcode 项目,使得其他项目成员在使用第三方库时无须任何额外的手工操作;

CocoaPods 已经成为 iOS 业界标准,支持几乎所有的开源库和商业库,即便是 Objective-C 的依赖库以及二进制文件(binary)依赖库,CocoaPods 也提供支持。

那么,怎样使用 CocoaPods 来管理第三方依赖库呢?接下来我会从语义化版本管理、Pod 版本管理、Pod 版本更新三个方面展开介绍。

语义化版本管理

开发软件,免不了要更新迭代,所以每一次更新的版本号管理变得很重要。并且,一旦版本号混乱,就会导致一系列问题,比如很难查找和修改线上崩溃,没办法支持多团队并行开发,等等。为了避免此类问题,我们可以使用语义化版本管理(Semantic Versioning)来统一版本号的定义规范。

语义化版本号是一种通用的版本号格式规范,目前绝大部分优秀的第三方依赖库都遵循这一规范来发布版本。

具体来说,语义化版本号的版本号一般包括四部分:MAJOR、MINOR、PATCH、BUILD。每一部分都由递增的数值组成,例如 1.2.3.4,其中 1 是MAJOR, 2 是 MINOR。如果我们更新 MINOR 版本号,那么下一个版本就是 1.3.0.0。接下来我详细介绍下这四部分。

  • MAJOR 是指主版本号,通常在重大更新的时候才会需要更新主版本号。例如 iOS 每年都会更新一个主版本号。而对于第三方库来说,主版本号的更新,表示该库的 API 新增了重大功能,或者引入了不可兼容的更新 (breaking changes)。

  • MINOR 是指副版本号,用于小功能的改善。例如 iOS 14 在发布主版本后,在一年内可能发布多个副版本如 14.1、 14.2 来完善其系统功能。一般对于第三方库来说,副版本的更新就是新增一些 API,但不包含不可兼容的更新。

  • PATCH 是指补丁版本号,一般用于 bug fix 以及修复安全性问题等。对于第三方库来说,补丁版本号的更新也不应该有不可兼容的更新。虽然实际操作中这会有些困难,但我们可以通过把原有 API 标记为 deprecated,或者为新 API 参数提供默认值等办法来解决。

  • BUILD 是指构建版本号,通常在内部测试时使用。一般当我们使用 CI 服务器进行自动构建时,构建版本号会自动更新。

Pod 版本管理

要使用 CocoaPods 管理第三方依赖库,首先要新建一个 Podfile 文件,然后执行bundle exec pod install命令来安装所有依赖库。这时候 CocoaPods 会自动帮我们建立一个 Podfile.lock 文件和一个 Workspace文档。

注意,在第一讲我们说过,由于是通过 Bundler 来安装 CocoaPods,每次执行pod命令前,都需要加上bundle exec。不过为了简洁,后面涉及pod命令时,我会省略bundle exec部分。

接下来,我详细介绍下 Podfile 文件、 Podfile.lock 和 Workspace 文档到底是什么,以及如何使用。

Podfile 文件

Podfile 文件是一个配置文件,它主要是用来描述 Xcode 项目里各个 target 的依赖库。我们项目的 Podfile 文件可以在仓库中找到。在这里,我主要和你介绍一下 Podfile 文件中的几个重要配置。

source 配置

source用于指向 PodSpec(Pod 规范)文件的 Repo,从而使得 CocoaPods 能查询到相应的 PodSpec 文件。

具体来说,当使用公共依赖库的时候,source需要指向 CocoaPods Master Repo,这个主仓库集中存放所有公共依赖库的 PodSpec 文件。 由于 CocoaPods 经常被开发者吐槽 Pod 下载很慢,因此 CocoaPods 使用了 CDN (Content Delivery Network,内容分发网络)来缓存整个 CocoaPods Master Repo, 方便开发者快速下载。具体的配置方法就是使source指向 CND 的地址,代码示例如下

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

如果使用的是私有依赖库,我们也需要把source指向私有库的 PodSpec Repo,以使得 CocoaPods 能找到相应的 PodSpec 文件。 代码示例如下:

source 'https://my-git-server.com/internal-podspecs'

注意,当我们使用私有库时,执行pod install命令的机器必须能访问到source所指向的 Repo。

projectworkspace

project用于指定我们的主项目文档。该项目文档会使用到 CocoaPods 管理的所有第三方依赖库。

workspace用于指定要生成和更新的 Workspace 文档。和其他依赖库管理工具不一样,CocoaPods 会自动生成一个 Workspace 文档,然后我们只能使用该文档而不是 Xcode 项目文档来进行后续开发。

project './Moments/Moments.xcodeproj'
workspace './Moments.xcworkspace'

这其中 Moments.xcodeproj 就是我们的主项目文档,它一般放在和项目名字相同的下一层目录下。

而 Moments.xcworkspace 是 CocoaPods 为我们生成的 Workspace文档,为了统一,我建议名字也是和主项目相同。

platformuse_frameworks

platform :ios, '14.0'
use_frameworks!

为了保证所有依赖库与主项目在编译和运行时兼容,我们指定的系统版本号需要和主项目所支持的系统版本号保持一致。而platform就是用于指定操作系统以及所支持系统的最低版本号。比如,例子中的platform :ios, '14.0'就表示支持 iOS 14.0 以上的所有 iOS 版本。

另外一行的use_frameworks!这一配置会让 CocoaPods 把所有第三方依赖库打包生成一个动态加载库,而不是静态库。因为动态库是我们经常用到的,它能有效地加快编译和链接的速度。

组织同类型的第三方依赖库

def dev_pods
  pod 'SwiftLint', '0.40.3', configurations: ['Debug']
  pod 'SwiftGen', '6.4.0', configurations: ['Debug']
end

其中configurations: ['Debug']用于指定该依赖库只是使用到Debug构建目标(target)里面,而不在其他(如Release)构建目标里面,这样做能有效减少 App Store 发布版本的体积。

def dev_pods end代码块是“复用同一类依赖库方式”的意思,我们可以把同类型的依赖库都放进这个代码块里面。比如,我们的 Moments 项目中就分别有 dev_pods(开发相关的库),core_pods(核心库)以及thirdparty_pods(第三方库)等代码块定义。

target 配置

有了这些复用库定义以后,怎样使用到项目的构建目标(target)里面呢?下面就是一个例子。

target 'Moments' do
  dev_pods
  core_pods
  # other pods...
end

我们可以把构建目标所使用的所有依赖库放进target代码块中间,上面中的Moments就是我们的 App 构建目标。该构建目标依赖了dev_podscore_pods等各组依赖库。执行pod install的时候,CocoaPods 会把dev_pods代码块自动展开为SwiftLintSwiftGen,那么Moments构建目标能使用SwiftLintSwiftGen依赖库了。

 pod 'RxSwift', '= 5.1.1'
 pod 'RxRelay', '= 5.1.1'

在 CocoaPods 里面,每一个依赖库称为一个 Pod (注意这里首字母大写,Pod 指一个库),指定一个 Pod 的命令是pod(注意这里是小写,表示一条命令)。在 Podfile 里面我们可以通过这样的格式pod 'RxSwift, = 5.1.1'来配置依赖库的版本号。其中,RxSwift或者RxRelay是依赖库的名字,5.1.1为版本号。这些库的名字以及版本号都可以在 CocoaPods 官网上找到。

为了统一管理第三方依赖库的版本,我建议统一使用 = 来锁定该依赖库的版本,这样就能保证每次执行pod install的时候都可以为同一个库下载同一个版本。

除了 = 操作符以外,CocoaPods 还支持其他操作符来指定版本:

  • > 0.1表示大于 0.1 的任何版本,这样可以包含 0.2 或者 1.0;

  • >= 0.1表示大于或等于 0.1 的任何版本;

  • < 0.1表示少于 0.1 的任何版本;

  • <= 0.1表示少于或等于 0.1 的任何版本;

  • ~> 0.1.2表示大于 0.1.2 而且最高支持 0.1.* 的版本,但不包含 0.2 版本。

这几个操作符相里面,~>(Squiggy arrow)操作符更为常用,它是以最后一个部分的版本号(例子中 0.1.2 的最后一个部分是补丁版本号 ..2)来计算可以支持的最高版本号。

例如~> 0.1.2表示 >= 0.1.2 并且 < 0.2.0,但不能等于 0.2.0, 因为 0.2.0 已经更新了副版本号而不仅仅是补丁版本号了。另外一个例子是~> 0.1,表示 >= 0.1 并且 < 1.0,举例来说,我们可以更新到 0.9 但不能更新到 1.0。

前面我介绍的是引用外部的第三方依赖库,如果我们的项目有自己的内部依赖库,要怎样在 CocoaPods 引用它呢?其实很简单,我们可以执行以下命令:

pod 'DesignKit', :path => './Frameworks/DesignKit', :inhibit_warnings => false

和其他外部依赖库不一样,我们需要使用:path来指定该内部库的路径。

Podfile.lock 文件

Podfile.lock 文件是由 CocoaPods 自动生成和更新的,该文件会详细列举所有依赖库具体的版本号。比如:

DEPENDENCIES:
  - Alamofire (= 5.2.0)
  - Firebase/Analytics (= 7.0.0)
PODFILE CHECKSUM: 400d19dbc4f5050f438797c5c6459ca0ef74a777

当执行pod install后,CocoaPods 会根据 Podfile 文件解释出各依赖库的特定版本号,然后一一列举在 DEPENDENCIES 下面。在上述的例子中,我们的 App 在构建过程中使用了5.2.0 的 Alamofire 库以及 7.0.0 的 Firebase Analytics 库。

PODFILE CHECKSUM 用于记录 Podfile 的验证码,任何库的版本号的更改,都会改变该验证码。这样能帮助我们在不同的机器上,快速检测依赖库的版本号是否一致。

我建议要把 Podfile 和 Podfile.lock 文件一同 commit 并 push 到 Git 代码管理服务器里面。特别是在团队开发的环境下,这样能帮助我们保证各个依赖库版本号的一致性。

在实践操作中,无论我们在哪台机器上执行pod install, PODFILE CHECKSUM 都不应该发生任何改变。因为我们在 Git 保存了 Podfile.lock,一旦我们发现老版本 App 的 Bug ,就可以根据该文件为各个依赖库重新安装同一版本号,来重现和定位问题,从而帮助我们快速修改这些 Bug。

Workspace 文档

Workspace 文档是 Xcode 管理子项目的方式。通过 Workspace,我们可以把相关联的多个 Xcode 子项目组合起来方便开发。

前面说过,当我们执行pod install的时候,CocoaPods 会自动创建或者更新一个叫作 Pods 的项目文档(Pods.xcodeproj )以及一个 Workspace 文档(在我们项目中叫作 Moments.xcworkspace)。

其中,Pods 项目文档负责统一管理各个依赖库,当我们在 Podfile 里面指定构建成动态库的时候,该项目会自动生成一个名叫Pods_<项目名称>.framework的动态库供我们项目使用。

而 Workspace 文档则统一管理了我们原有的主项目 (Moments.xcodeproj)以及那个 Pods 项目。

与此同时,CocoaPods 还会修改 Xcode 项目中的 Build Phases 以此来检测 Podfile.lock 和 Manifest.lock 文件的一致性,并把Pods_<项目名称>.framework动态库嵌入我们的主项目中去。

以上所有操作都是由 CocoaPods 自动帮我们完成。以后的开发,我们都可以打开 Workspace 文档而不是原有的 Xcode 项目文档来进行。

Pod 版本更新

使用 CocoaPods 管理第三方依赖库的操作非常简单,可是一旦使用不当,特别是在 Pod 更新的时候,很容易引起依赖库版本不一致,从而出现各种问题。

比如,在编译程序的时候,有些开发者可以顺利进行,而另外一些开发者编译时候就会出错;或者程序在本地编译时运行良好,一旦在 CI 上构建,就会出现 App 崩溃,等等。

那么,怎么保证更新 Pod 的时候都能保证版本一致呢?

下面结合我的实践经验,以第三方网络库 Alamofire 为例子和你介绍下。

第一步,CocoaPods 已经为我们提供了pod outdated命令,我们可以用它一次查看所有 Pod 的最新版本,而无须到 GitHub 上逐一寻找。下面是执行pod outdated命令的其中一条结果:

The following pod updates are available:
- Alamofire 5.2.0 -> 5.2.0 (latest version 5.4.0)

这表示当前我们使用了版本为 5.2.0 的 Alamofire ,其最新版本为 5.4.0。如果我们决定更新到版本 5.4.0,那么可以继续下一步。

第二步,在更新依赖库版本之前,为了避免在新版本中不小心引入 Bug,我们需要了解新的版本到底提供了哪些新功能,修改了哪些 Bug,与老版本是否兼容等事项。具体我们可以到 CocoaPods 官网上查找需要更新的第三方依赖库,然后在 GitHub 等平台上找到,并仔细阅读该库的版本说明(release note)。

请注意,我们要阅读当前使用版本到要更新的版本之间的所有版本说明。 在这个例子中,我们要阅读 5.2.1,5.2.2,5.3.0 和 5.4.0 的所有版本说明。这些版本说明会列出新增功能,更新的 API,修改的 Bug,有没有不可兼容的更新 。

第三步,在 Podfile 文件里把要更新的 Pod 的版本号进行修改。例如把pod 'Alamofire', '= 5.2.0'改成pod 'Alamofire', '= 5.4.0'。 然后执行pod install来重新生成 Podfile.lock 文件。

此时特别注意的是,我们要使用pod install而不是pod update。因为执行pod update会自动更新所有 Pod 的版本,这可能会更新了一些我们目前还不想更新的 Pod,从而会引入一些难以觉察的问题。

第四步,如果所更新的版本包含了不可兼容的更新,我们需要修改代码来保证代码能顺利完成编译。

第五步,很多第三方依赖库都是一些通用的基础组件,一旦发生问题会影响到整个 App 的功能,因此我们需要根据所更新的库进行回归测试。例如当更新了 Alamofire 库的时候,我们需要把每个网络请求都执行一遍,避免所更新的版本引入新的 Bug。

第六步,为了把更新的版本共享给所有开发者和 CI 服务器,我们需要把 Podfile 和 Podfile.lock 文件一同 commit 并 push 到 Git 代码管理服务器,并通过 Pull Request 流程并入主分支。

第七步,一旦更新的代码并入主分支后,要通过 Slack 等内部通信软件告诉所有开发者 pull 或者 rebase 主分支的代码,并执行pod install 来更新他们开发环境的所有依赖库。

特别注意,千万不要使用pod update,因为pod update会自动把开发者机器上所有 Pod 的版本自动更新了。这种更新往往不是我们想要的结果,我们希望统一更新各个 Pod 的版本,并通过 Git 进行集中管理。

如果开发者在编译新代码前没有执行pod install命令,会出现以下的错误

The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.

这错误可以有效提醒所有开发者,需要再次执行pod install来更新他们本地的依赖库,从而保证所有开发者使用的依赖库的版本都是一致的。

另外,如果更新了基础组件的依赖库(如网络库),在测试阶段,我们还需要进行全面的回归测试。因为这些基础组件库的新版本如果有 Bug 很可能导致我们的 App 会发生大比例的崩溃,严重影响用户的体验。

有了上面的一流程,我们就可以有效地保证每个开发者使用的依赖库版本都是一致的,同时也能保证 CI 在自动构建 App 的时候所使用的依赖库版本也是统一的。

总结

这一讲我介绍了如何使用 CocoaPods 来统一管理依赖库的版本。特别是根据我自己的经验总结了一套更新 Pod 版本的流程,希望你灵活使用这些步骤,从而少走弯路。


image.png

这里我再特别强调一下,为了保证依赖库版本都能保持一致,尽量不要执行pod update,而是使用通过修改 Podfile 文件里的版本号并执行pod install来更新 Pod 的版本,然后把 Podfile 和 Podfile.lock 文件一同并入 Git 主分支中进行统一管理。

思考题:

CocoaPods 非常简单易用,它可以同时管理依赖库的依赖项,例如我们的 App 依赖 A 库, 而 A 库又依赖 B 库,同时 B 库依赖 C 库,CocoaPods 可以帮我们自动找出所有依赖项并按顺序安装所有依赖库。 那你知道 CocoaPods 是如何管理依赖库的依赖呢?

源码地址:

Podfile 文件地址:
https://github.com/lagoueduCol/iOS-linyongjian/blob/main/Podfile

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容