文章比较长,因此在文章的开头我打算简单介绍一下这篇文章将要讲述的内容,读者能够选择通篇细度,也能够直接找到本身感兴趣的部分。php
既然是谈 Cocoapods,那首先要搞明白它出现的背景。有经验的开发者都知道 Cocoapods 在实际使用中,常常遇到各类问题,存在必定的使用成本,所以衡量 Cocoapods 的成本和收益就显得很关键。css
Cocoapods 的本质是一套自动化工具。那么了解自动化流程背后的原理就很重要,若是咱们能手动的模拟 Cocoapods 的流程,不管是对 Cocoapods 仍是 Xcode 工程配置的学习都大有裨益。好比以前曾经和同事研究过静态库嵌套的问题,很遗憾当时没能解决,如今想来仍是对相关知识理解还不够到位。这一部分主要是介绍 Xcode 的工程配置,以及 target/project/workspace 等名词的概念。ios
最后,我会结合实际的例子,谈谈如何发布本身的 Pod,提供给别人使用。算是对 Cocoapods 的实践总结。git
因为实践性的操做比较多,我为本文制做了一个 demo,提交在 个人 Github: CocoaPodsDemo 上,感兴趣的读者能够下载下来,研究一下提交历史,或者本身操做一遍。友情提醒: 本文所涉及的静态库均为模拟器制做,请勿真机运行。github
咱们知道,再大的项目最初都是从 Xcode 提供的一个很是简单的工程模板慢慢演化来的。在项目的演化过程当中,为了实现新的功能,不断有新的类被建立,新的代码被添加。不过除了本身添加代码,咱们也常常会直接把第三方的开源代码导入到项目中,从而避免重复造轮子,节约开发时间。swift
直接把代码导入到项目中看起来很容易,但在实践过程当中,会遇到诸多问题。这些问题会困扰代码的使用者,大大的增长了集成代码的难度。vim
最直接的问题就是代码的后续维护。假设代码的发布者在将来的某一天更新了代码,修复了一个重大 bug 或者提供了新的功能,那么使用者就很难集成这些变更。xcode
代码有增有删,若是把代码编译成静态库再提供给使用者, 就能够省掉不少问题。然而若是这么作的话,就会遇到另外一个经典的问题: "Other linker flag"。缓存
举个例子来讲,能够在 Demo 的 BSStaticLibraryOne
这个项目中看到,这个静态库一共有两个类,其中一个是拓展 Extension。项目编译后就会获得一个 .a
文件。ruby
咱们都知道静态库的格式能够是 .framework
,也能够是 .a
。若是深究的话,.a
文件能够理解为一种归档文件,或者说是压缩文件。其中存储的是通过编译的 .o
格式的目标文件。咱们能够经过 ar -x
命令来证实这一点:
ar -x libBSStaticLibraryOne.a
须要提醒的一点是,光有 .a
文件还不够,咱们还须要提供头文件给使用者导入。为了完成这一点,咱们须要在项目的 Build Phases 中新增一个 Headers Phase,而后把须要对外暴露的头文件放到 Public 一栏中:
此时编译后的头文件会放在 .a
文件所在目录下,usr/local/include
目录中。
接下来打开 OtherLinkerFlag
这个壳工程,引入 .a
文件和头文件,运行程序,结果必定是:
-[BSStaticLibraryOne sayOtherThing]: unrecognized selector sent to instance xxx
这就是经典的 linker flag
问题。首先,咱们知道 .a
实际上是编译好的目标文件的集合,所以问题出在连接这一步,而非编译。Objective-C 在使用静态库时,须要知道哪些文件须要连接进来,它依据的就是以前图中所示的 __.SYMDEF SORTED
文件。
惋惜的是,这个文件不会包含全部的 .o
目标文件,而只是包含了定义了类的目标文件。咱们能够执行 cat __.SYMDEF\ SORTED
来验证一下,你会看到其中并无拓展类的信息。这样一来,BSStaticLibraryOne+Extension.o
虽然存在,可是不被连接到最终的可执行文件中,从而致使了找不到方法的错误。
解决上述问题的方法是调用者在 Build Settings
中找到 other linker flag
,并写上 -ObjC
选项,这个选项会连接全部的目标文件。然而根据文档描述,若是静态库只有分类,而没有类, 即便加了 -ObjC
选项也会报错,应该使用 -force_load
参数。
因为第三方的代码使用分类几乎是必然事件,所以几乎每一个使用者都要作如上配置,增长了复杂度和出错的概率。
除此之外,第三方的代码颇有可能使用了系统的动态库。所以使用者还必须手动引入这些动态库(请记住这一点,静态库不支持递归引用,这是个很麻烦的事情,后面会介绍),咱们以百度地图 SDK 的集成为例,读者能够自行对比手动导入和 Cocoapods 集成的步骤区别: 配置开发环境iOS SDK。
所以,我总结的使用 Cocoapods 的好处有以下几个:
在我以前的一篇文章: 白话 Ruby 与 DSL 以及在 iOS 开发中的运用 中简单的介绍过,Cocoapods 是用 Ruby 开发的一套工具。每一份代码都是一个 Pod,安装 Pod 时首先会分析库的版本和依赖关系,这些都是在 Ruby 层面完成的,本文暂且不表。
咱们首先假设已经找到了要下载的代码的地址(好比存在 Github 上),从这一步开始,接下来的工做都与 iOS 开发有关。
若是你手头有一个 Cocoapods 项目,你应该会注意到如下几个特色:
libPods.a
这个 Cocoapods 库这样作能够把引入第三方库对主工程形成的影响降到最低,不过没法彻底降为零。好比引入 Cocoapods 之后,项目不得不使用 xworkspace
来打开,后面会介绍缘由。
假设以前的 BSStaticLibraryOne
工程就是下载好的源码,如今咱们要作的就是把它集成到一个已有的工程,好比叫 ShellProject
中。
咱们遇到的第一个问题是,在以前的 demo 中,须要把静态库和头文件手动拖入到工程中。但这就和 Cocoapods 的效果不一致,毕竟咱们但愿主工程彻底不受影响。
若是咱们什么都不作,固然不可能在壳工程中引用另外一个项目下的静态库和头文件。但这个问题也能够换个方式问:“Xcode 怎么知道它们能够引用,仍是不能够引用呢?”,答案在于 Build Settings 里面的 Search Paths 这一节。默认状况下,Header Search Path和 Library Search Path 都是空的,也就是说 Xcode 不会去任何目录下找静态库和头文件,除非他们被人为的导入到工程中来。
所以,只要对上述两个选项的值略做修改, Xcode 就能够识别了。咱们目前的项目结构以下所示:
- CocoaPodsDemo(根目录) - BSStaticLibraryOne (被引用的静态库) - Build/Products/Debug-iphonesimulator (编译结果的目录) - libBSStaticLibraryOne.a (静态库) - usr/local/include (头文件目录) - BSStaticLibraryOne.h - BSStaticLibraryOne+Extension.h - ShellProject (壳工程)
所以咱们要作的是让壳工程的 Library Search Path 指向CocoaPodsDemo/BSStaticLibraryOne/Build/Products/Debug-iphonesimulator 这个目录:
Library Search Path = $PROJECT_DIR/../BSStaticLibraryOne/Build/Products/Debug-iphonesimulator/
这里记得写相对路径,Xcode 会自动转成绝对路径。而后 Header Search Path 也如法炮制:
Header Search Path = $PROJECT_DIR/../BSStaticLibraryOne/Build/Products/Debug-iphonesimulator/LibOne
细心的读者也许会发现, LibOne 这个文件夹彻底不存在。是这样的,由于我以为usr/local/include 这个路径太深,太丑,因此能够在静态库的项目配置中,在Packaging 这一节中,找到 Public Headers Folder Path,将它的值从usr/local/include 修改成 LibOne,而后从新编译,这时就会看到生成的头文件位置发生了变化。
固然,这时候仍是没法直接引用静态库的。由于咱们只是告诉 Xcode 能够去对应路径去找,但并无明确声明要用,因此须要在 Other Linker Flags 中添加一个选项: -l"BSStaticLibraryOne"
,引号中的内容就是静态库的工程名。
须要提醒的是, 静态库编译出来的 .a
文件会被手动加上 lib
前缀,在写入到 Other Linker Flags 的时候千万要注意去掉这个前缀,不然就会出现 Library not found 的错误。
配置好之后的工程以下图所示:
如今项目中没有任何第三方的库或者代码,依然能够正常引用第三方的类并运行成功。
当咱们的项目须要引用多个第三方库的时候,就有两种思路:
从直觉来看,第二种组织方式看上去更加集中,易于管理。考虑后面咱们还要解决库的依赖问题,并且项目内的依赖处理比 workspace 中的依赖处理要容易不少(后面会介绍到),因此第二种组织方式更具备可行性。
若是读者手头有使用了 Cocoapods 的项目,能够看到它的文件组织结构以下:
- ShellProject(根目录,壳工程) - ShellProject (项目代码) - ShellProject.xcodeproj (项目文件) - Pods (第三方库的根目录) - Pods.xcodeproj (第三方库的总工程) - AFNetworking (某个第三方库) - Mantle (另外一个第三方库) - ……
而在个人 demo 中,为了偷懒,没有把第三方库放在壳工程目录下,而是选择和它平级。这其实没有太大的区别,只是引用路径不一样而已,不用太关心。咱们如今模拟添加一个新的第三方库,完成后的代码结构以下:
- CocoaPodsDemo(根目录) - BSStaticLibraryOne (第三方库总的文件夹,至关于 Pods,由于偷懒,名字就不改了) - BSStaticLibraryOne (第一个第三方库) - BSStaticLibraryTwo (新增一个第三方库) - BSStaticLibraryOne.xcodeproj (第三方库的项目文件) - Build/Products/Debug-iphonesimulator (编译结果的目录) - ShellProject (壳工程)
首先要新建一个文件夹 BSStaticLibraryTwo 并拖入到项目中,而后新增一个 Target(以下图所示)。
在 Xcode 工程中,咱们都接触过 Project。打开 .xcodeproj 文件就是打开一个项目(Project)。Project 负责的是项目代码管理。一个 Project 能够有多个 Target,这些 target 可使用不一样的文件,最后也就能够得出不一样的编译产物。
经过使用多个 target,咱们能够用少量不一样的代码获得不一样的 app,从而避免了开多个工程的必要。不过咱们这里的几个 target 并不含有相同代码,而是一个第三方库对应一个 target。
接下来咱们新建一个类,记得要加入到 BSStaticLibraryTwo 这个 target 下,记得和以前同样修改 Public Headers Folder Path 并添加一个 Build Phase。
在左上角将 Scheme 选择为 BSStaticLibraryTwo 再编译,能够看到新的静态库已经生成了。
对于主工程来讲,必须在子工程(第三方库)编译完后才开始编译,或者换句话说,咱们在主工程中按下 Command + R/B 时,全部子工程必须先被编译。对于这种跨工程的库依赖,咱们没法直接指明依赖关系,必须隐式的设置依赖关系,咱们仍是以 Cocoapods 工程举例:
主工程中用到了 libPod.a 这个静态库,并且它并非在主工程中生成,而是在 Pods 这个项目中编译生成。一旦存在这种引用关系,那么也就创建了隐式的依赖关系。在编译主工程时,Xcode 会确保它引用的全部静态库都先被编译。
以前咱们讨论过两种管理多个静态库的方法,若是选择第一种方法, 每一个静态库对应一个 Xcode 项目,虽然不是不能够,但主工程看上去就就会比较复杂,这主要是跨项目依赖致使的。
而在项目内部管理 target 的依赖相对而言就简单不少了。咱们只要新建一个总的 target,不妨也叫做 Pod。它什么也不作,只须要依赖另外两个静态库就能够了,设置 Target Dependencies:
此时选择 Pod 这个 target 编译,另外两个静态库也会被编译。所以接下来的任务就是让主工程直接依赖于 Pod 这个 target,天然也就间接依赖于真正有用的各个第三方静态库了。
接下来咱们重复以前的步骤,设置好头文件和静态库的搜索路径,并在 Other Linker Flags 里面添加: -l"BSStaticLibraryTwo"
,就可使用第二个静态库了。
到目前为止,咱们模拟了多个静态库的组织,以及如何在主工程中引用他们。不过还存在一些小瑕疵,我截了 Xcode 中的一幅图:
从图中能够很明显的发现: 第三方库中的代码被认为是系统代码,颜色为蓝色。而正常的自定义方法应该绿色,会对开发者形成困扰。
除了这个小瑕疵之外,在以前谈到的跨项目依赖中,一个项目不只仅须要引用另外一个项目的产物,还有一个先决条件: 把这两个项目放入同一个 Workspace 中。Workspace 的做用是组织多个 Project,使得各个 Project 直接能够有引用依赖关系,同时也能让 Xcode 识别出各个 Project 中的代码和头文件。
按住 Command + Control + N 能够新建一个 Workspace:
完成之后就会看到一个彻底空白的项目,在左侧按下右键,选择 Add Files to:
而后选中静态库项目和主工程的 .xcodeproj 文件,把这两个工程都加进来:
须要提醒的是,切换到 Workspace 之后, Xcode 会把 Workspace 所在目录当作项目根目录,所以静态库的编译结果会放在 /CocoaPodsDemo/Build/Products/...,而再也不是以前的 /CocoaPodsDemo/BSStaticLibraryOne/Build/Products/...,所以须要手动对主工程中的搜索路径作一下调整。
作好上述改动后,即便咱们删除掉 BSStaticLibraryOne 这个项目的编译结果,只在 Workspace 中编译主项目,Xcode 也会自动为咱们编译被依赖的静态库。这就是为何咱们只须要执行 pod install
下载好代码,就能够不用作别的操做,直接在主项目中运行。
固然,代码颜色错误的小问题也在 Workspace 恢复正常了。
到这里,基本上关于 Cocoapods 的工做原理就算是分析完了。上述操做除了文件增长,基本上都是修改 .pbxproj 文件。全部的 Xcode 都会在该文件中获得反映,同理,只要修改该文件,也能达到上述手动操做的效果。而 Cocoapods 开发了一套 Ruby 工具,用来封装这些修改,从而实现了自动化。
文章开头,咱们提到做为代码提供者,若是本身的代码还引用别的第三方库,那么提供代码会变得很麻烦,这主要是因为静态库不会递归引用致使的。咱们已经知道静态库其实就是一堆编译好的目标文件(.o 文件)的打包形式,它须要配合头文件来使用。所谓的不会递归引用是指,假设项目 A 引用了静态库 B(或者是动态库,也是同样),那么 A 编译后获得的静态库中,并不含有静态库 B 的目标文件。若是有人拿到这样的静态库 A,就必须补齐静态库 B,不然就会遇到 "Undefined symbol" 错误。
若是咱们提供的代码引用了系统的动态库,问题还比较简单,只要在文档里面注明,让使用者本身导入便可。但若是是第三方代码,那么这简直是一块儿灾难。即便使用者找到了提供者使用的静态库,那个静态库也颇有可能已经进行了升级,而版本不一致的静态库可能具备彻底不一样的 API。也就是说代码提供者还要在文档中注明使用的静态库的版本,而后由使用者去找到这个版本。我想,这才是 Cocoapods 真正致力于解决的任务。
CocoaPods 的作法比较简单,由于他有一套统一的版本表示规则,也能够自动分析依赖关系,并且每一个版本的代码都有记录。后面会介绍 Cocoapods 的相关实践,这里咱们先思考一下如何手动解决静态库嵌套的问题。
既然静态库只是目标文件的打包形式,那么我只须要找到被嵌套的静态库,拿到其中的目标文件,而后和外层的静态库放在一块儿从新打包便可。这个过程比较简单, 我也就没有作 demo,用代码应该就能够说明得很清楚。假设咱们有静态库 A.a 和 B.a,其中 A 须要引用 B,如今我但愿对外发布 A,而且集成 B:
lipo A.a -thin x86_64 output A_64.a # 若是是多 CPU 架构,先提取出某一种架构下的 .a 文件 lipo B.a -thin x86_64 output B_64.a ar -x A_64.a # 解压 A 中的目标文件 ar -x B_64.a # 解压 B 中的目标文件 libtool -static -o Together.a *.o # 把全部 .o 文件一块儿打包到 Together.a 中
这时候 Together.a 文件就能够当作完整版的静态库 A 给别人使用了。
原本 Cocoapods 的使用就比较简单。尤为是了解完原理后,使用起来应该更加驾轻就熟了,对于一些常见的错误也有了分析能力。不过有个小细节仍是须要注意一下:
关于 Cocoapods 文件是否要加入版本控制并无明确的答案。我之前的习惯是不加入版本控制。由于这样会让提交历史明显变得复杂,若是不一样分支上使用的不一样版本的 pod,在合并分支时就会出现大量冲突。
然而官方的推荐是把它加入到版本控制中去。这样别人再也不须要执行 pod install
,并且可以确保全部人的代码必定一致。
然而虽然不强制把整个 Pod 都加入版本控制,可是 Podfile.lock 不管如何须须添加到版本控制系统中。为了解释这个问题,咱们先来看看 Cocoapods 可能存在的问题。
假设咱们在 Podfile 中写上: pod 'AFNetWorking'
,那么默认是安装 AFNetworking 的最新代码。这就致使用户 A 可能装的是 3.0 版本,而用户 B 再安装就变成了 4.0 版本。即便咱们在 Podfile 中指定了库的具体版本,那也不能保证不出问题。由于一个第三方库还有可能依赖其余的第三方库,并且不保证它的依赖关系是具体到版本号的。
所以 Podfile.lock 存在的意义是将某一次 pod install
时使用的各个库的版本,以及这个库依赖的其余第三方库的版本记录下来,以供别人使用。这样一来,pod install
的流程实际上是:
而另外一个经常使用命令 pod update
并非一个平常更新命令。它的原理是忽略 Podfile.lock 文件,彻底使用 Podfile 中的配置,而且更新 Podfile.lock。一旦决定使用 pod update
,就必须全部团队成员一块儿更新。所以在使用 update
前请务必了解其背后发生的事情和对团队形成的影响,而且确保有必要这么作。
不少教程都有介绍开源 Pod 的流程,我在实践的时候主要参考了如下两篇文章。相对来讲比较详细,条理清晰,也推荐给你们:
若是要建立公司内部的私有库,首先要创建一个本身的仓库,这个仓库在本地也会有存储:
如图中所示,master 是官方仓库,而 baidu 则是我用来测试的私有仓库。仓库中会存有全部 Pod 的信息,每一个文件夹下都按照版本号作了区分,每一个版本对应一个 podspec 文件。从图中能够看到,cocoapods 会缓存全部的 podspec 到本地,但不会缓存每一个 Pod 的具体代码。每当咱们执行 pod install
时,都会先从本地查找 podspec 缓存是否存在,若是不存在则会去中央仓库下载。
咱们常常遇到的 pod install
很慢就是由于默认状况下会更新整个 master。此时 master 不只仅存储着本地使用 Pod 的 PodSpec 文件,而是存储了全部的已有的 Pod。因此这个更新过程看起来异常缓慢。有些解决方案是使用:
pod install --verbose --no-repo-update
这实际上是治标不治本的姑息治疗方法,由于本地的仓库早晚要被更新,不然就拿不到最新的 PodSpec。要想完全解决这一问题,除了按期更新外,还能够选择其余速度较快的镜像仓库。
podspec 文件是咱们开源 Pod 时须要填写的文件,主要是描述了 Pod 的基础信息。除了一些可有可无的配置和介绍信息外,最重要的填写 source_files 和 dependency。前者用来规定哪些文件会对外公布,后者则指定此 Pod 依赖于哪些其余 Pod。好比在上图中,个人 PrivatePod 就依赖于 CorePod,在公司内部的项目中使用 PodS 依赖能够大量简化代码的集成流程。一个典型的 PodSpec 可能长这样:
填写好上述信息后,咱们只要先 lint 一下 podspec,确保格式无误,就能够提交了。