国内最全IT社区平台 联系我们 | 收藏本站
华晨云阿里云优惠2
您当前位置:首页 > php开源 > 综合技术 > 细聊 Cocoapods 与 Xcode 工程配置

细聊 Cocoapods 与 Xcode 工程配置

来源:程序员人生   发布时间:2018-06-11 16:41:02 阅读次数:5981次

前言

文章比较长,所以在文章的开头我打算简单介绍1下这篇文章将要讲述的内容,读者可以选择通篇细度,也能够直接找到自己感兴趣的部份。

既然是谈 Cocoapods,那首先要弄明白它出现的背景。有经验的开发者都知道 Cocoapods 在实际使用中,常常遇到各种问题,存在1定的使用本钱,因此衡量 Cocoapods 的本钱和收益就显得很关键。

Cocoapods 的本质是1套自动化工具。那末了解自动化流程背后的原理就很重要,如果我们能手动的摹拟 Cocoapods 的流程,不管是对 Cocoapods 还是 Xcode 工程配置的学习都大有裨益。比如之前曾和同事研究过静态库嵌套的问题,很遗憾当时没能解决,现在想来还是对相干知识理解还不够到位。这1部份主要是介绍 Xcode 的工程配置,和 target/project/workspace 等名词的概念。

最后,我会结合实际的例子,谈谈如何发布自己的 Pod,提供给他人使用。算是对 Cocoapods 的实践总结。

由于实践性的操作比较多,我为本文制作了1个 demo,提交在 我的 Github: CocoaPodsDemo 上,感兴趣的读者可以下载下来,研究1下提交历史,或自己操作1遍。友谊提示: 本文所触及的静态库均为摹拟器制作,请勿真机运行。

为何要使用 Cocoapods

我们知道,再大的项目最初都是从 Xcode 提供的1个非常简单的工程模板渐渐演变来的。在项目的演变进程中,为了实现新的功能,不断有新的类被创建,新的代码被添加。不过除自己添加代码,我们也常常会直接把第3方的开源代码导入到项目中,从而避免重复造轮子,节俭开发时间。

直接把代码导入到项目中看起来很容易,但在实践进程中,会遇到诸多问题。这些问题会困扰代码的使用者,大大的增加了集成代码的难度。

使用者的困扰

最直接的问题就是代码的后续保护。假定代码的发布者在未来的某1天更新了代码,修复了1个重大 bug 或提供了新的功能,那末使用者就很难集成这些变动。

代码有增有删,如果把代码编译成静态库再提供给使用者, 就能够省掉很多问题。但是如果这么做的话,就会遇到另外一个经典的问题: “Other linker flag”。

举个例子来讲,可以在 Demo 的 BSStaticLibraryOne 这个项目中看到,这个静态库1共有两个类,其中1个是拓展 Extension。项目编译后就会得到1个 .a 文件。

我们都知道静态库的格式可以是 .framework,也能够是 .a。如果深究的话,.a 文件可以理解为1种归档文件,或说是紧缩文件。其中存储的是经过编译的 .o 格式的目标文件。我们可以通过 ar -x 命令来证明这1点:

ar -x libBSStaticLibraryOne.a

解压结果

需要提示的1点是,光有 .a 文件还不够,我们还需要提供头文件给使用者导入。为了完成这1点,我们需要在项目的 Build Phases 中新增1个 Headers Phase,然后把需要对外暴露的头文件放到 Public 1栏中:

暴露头文件

此时编译后的头文件会放在 .a 文件所在目录下,usr/local/include 目录中。

接下来打开 OtherLinkerFlag 这个壳工程,引入 .a 文件和头文件,运行程序,结果1定是:

-[BSStaticLibraryOne sayOtherThing]: unrecognized selector sent to instance xxx

这就是经典的 linker flag 问题。首先,我们知道 .a 实际上是编译好的目标文件的集合,因此问题出在链接这1步,而非编译。Objective-C 在使用静态库时,需要知道哪些文件需要链接进来,它根据的就是之前图中所示的 __.SYMDEF SORTED 文件。

惋惜的是,这个文件不会包括所有的 .o 目标文件,而只是包括了定义了类的目标文件。我们可以履行 cat __.SYMDEF\ SORTED 来验证1下,你会看到其中并没有拓展类的信息。这样1来,BSStaticLibraryOne+Extension.o 虽然存在,但是不被链接到终究的可履行文件中,从而致使了找不到方法的毛病。

解决上述问题的方法是调用者在 Build Settings 中找到 other linker flag,并写上 -ObjC 选项,这个选项会链接所有的目标文件。但是根据文档描写,如果静态库只有分类,而没有类, 即便加了 -ObjC 选项也会报错,应当使用 -force_load 参数。

由于第3方的代码使用分类几近是必定事件,因此几近每一个使用者都要做如上配置,增加了复杂度和出错的概率。

除此之外,第3方的代码很有可能使用了系统的动态库。因此使用者还必须手动引入这些动态库(请记住这1点,静态库不支持递归援用,这是个很麻烦的事情,后面会介绍),我们以百度地图 SDK 的集成为例,读者可以自行对照手动导入和 Cocoapods 集成的步骤区分: 配置开发环境iOS SDK。

因此,我总结的使用 Cocoapods 的好处有以下几个:

  1. 避免直接导入文件的原始方式,方便后续代码升级
  2. 简化、自动化集成流程,避免没必要要的配置
  3. 自动处理库的依赖关系
  4. 简化开发者发布代码流程

Cocoapods 工作原理

在我之前的1篇文章: 白话 Ruby 与 DSL 和在 iOS 开发中的应用 中简单的介绍过,Cocoapods 是用 Ruby 开发的1套工具。每份代码都是1个 Pod,安装 Pod 时首先会分析库的版本和依赖关系,这些都是在 Ruby 层面完成的,本文暂且不表。

我们首先假定已找到了要下载的代码的地址(比如存在 Github 上),从这1步开始,接下来的工作都与 iOS 开发有关。

如果你手头有1个 Cocoapods 项目,你应当会注意到以下几个特点:

  1. 主工程中没有导入第3方库的代码或静态库
  2. 主工程不显式的依赖各个第3方库,但是援用了 libPods.a 这个 Cocoapods 库
  3. 不需要手动编译第3方库,直接运行主工程便可,隐式指定了编译顺序

这样做可以把引入第3方库对主工程酿成的影响降到最低,不过没法完全降为零。比如引入 Cocoapods 以后,项目不能不使用 xworkspace 来打开,后面会介绍缘由。

假定之前的 BSStaticLibraryOne 工程就是下载好的源码,现在我们要做的就是把它集成到1个已有的工程,比如叫 ShellProject 中。

我们遇到的第1个问题是,在之前的 demo 中,需要把静态库和头文件手动拖入到工程中。但这就和 Cocoapods 的效果不1致,毕竟我们希望主工程完全不受影响。

静态库和头文件导入

如果我们甚么都不做,固然不可能在壳工程中援用另外一个项目下的静态库和头文件。但这个问题也能够换个方式问:“Xcode 怎样知道它们可以援用,还是不可以援用呢?”,答案在于 Build Settings 里面的 Search Paths 这1节。默许情况下,Header Search PathLibrary 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 这1节中,找到 Public Headers Folder Path,将它的值从 usr/local/include 修改成 LibOne,然后重新编译,这时候就会看到生成的头文件位置产生了变化。

固然,这时候候还是没法直接援用静态库的。由于我们只是告知 Xcode 可以去对应路径去找,但并没有明确声明要用,所以需要在 Other Linker Flags 中添加1个选项: -l"BSStaticLibraryOne",引号中的内容就是静态库的工程名。

需要提示的是, 静态库编译出来的 .a 文件会被手动加上 lib 前缀,在写入到 Other Linker Flags 的时候千万要注意去掉这个前缀,否则就会出现 Library not found 的毛病。

配置好以后的工程以下图所示:

配置搜索路径

现在项目中没有任何第3方的库或代码,仍然可以正常援用第3方的类并运行成功。

援用多个第3方库

当我们的项目需要援用多个第3方库的时候,就有两种思路:

  1. 每份第3方代码作为1个工程,分别打出1个静态库和头文件。
  2. 所有第3方代码放在同1个工程中,建立多个 target,每一个 target 对应1个静态库。

从直觉来看,第2种组织方式看上去更加集中,易于管理。斟酌后面我们还要解决库的依赖问题,而且项目内的依赖处理比 workspace 中的依赖处理要容易很多(后面会介绍到),所以第2种组织方式更具有可行性。

如果读者手头有使用了 Cocoapods 的项目,可以看到它的文件组织结构以下:

- ShellProject(根目录,壳工程)
    - ShellProject (项目代码)
    - ShellProject.xcodeproj (项目文件)
    - Pods (第3方库的根目录)
        - Pods.xcodeproj (第3方库的总工程)
        - AFNetworking (某个第3方库)
        - Mantle (另外一个第3方库)
        - ……

而在我的 demo 中,为了偷懒,没有把第3方库放在壳工程目录下,而是选择和它平级。这其实没有太大的区分,只是援用路径不同而已,不用太关心。我们现在摹拟添加1个新的第3方库,完成后的代码结构以下:

- CocoaPodsDemo(根目录)
    - BSStaticLibraryOne (第3方库总的文件夹,相当于 Pods,由于偷懒,名字就不改了)
        - BSStaticLibraryOne (第1个第3方库)
        - BSStaticLibraryTwo (新增1个第3方库)
        - BSStaticLibraryOne.xcodeproj (第3方库的项目文件)
        - Build/Products/Debug-iphonesimulator (编译结果的目录)
    - ShellProject (壳工程)

首先要新建1个文件夹 BSStaticLibraryTwo 并拖入到项目中,然后新增1个 Target(以下图所示)。

新增 target

在 Xcode 工程中,我们都接触过 Project。打开 .xcodeproj 文件就是打开1个项目(Project)。Project 负责的是项目代码管理。1个 Project 可以有多个 Target,这些 target 可使用不同的文件,最后也就能够得出不同的编译产物。

通过使用多个 target,我们可以用少量不同的代码得到不同的 app,从而避免了开多个工程的必要。不过我们这里的几个 target 其实不含有相同代码,而是1个第3方库对应1个 target。

接下来我们新建1个类,记得要加入到 BSStaticLibraryTwo 这个 target 下,记得和之前1样修改 Public Headers Folder Path 并添加1个 Build Phase

代码添加到另一个 Target

在左上角将 Scheme 选择为 BSStaticLibraryTwo 再编译,可以看到新的静态库已生成了。

项目内依赖

对主工程来讲,必须在子工程(第3方库)编译完后才开始编译,或换句话说,我们在主工程中按下 Command + R/B 时,所有子工程必须先被编译。对这类跨工程的库依赖,我们没法直接指明依赖关系,必须隐式的设置依赖关系,我们还是以 Cocoapods 工程举例:

跨工程依赖

主工程中用到了 libPod.a 这个静态库,而且它其实不是在主工程中生成,而是在 Pods 这个项目中编译生成。1旦存在这类援用关系,那末也就建立了隐式的依赖关系。在编译主工程时,Xcode 会确保它援用的所有静态库都先被编译。

之前我们讨论过两种管理多个静态库的方法,如果选择第1种方法, 每一个静态库对应1个 Xcode 项目,虽然不是不可以,但主工程看上去就就会比较复杂,这主要是跨项目依赖致使的。

而在项目内部管理 target 的依赖相对而言就简单很多了。我们只要新建1个总的 target,无妨也叫作 Pod。它甚么也不做,只需要依赖另外两个静态库就能够了,设置 Target Dependencies:

此时选择 Pod 这个 target 编译,另外两个静态库也会被编译。因此接下来的任务就是让主工程直接依赖于 Pod 这个 target,自然也就间接依赖于真正有用的各个第3方静态库了。

接下来我们重复之前的步骤,设置好头文件和静态库的搜索路径,并在 Other Linker Flags 里面添加: -l"BSStaticLibraryTwo",就能够使用第2个静态库了。

Workspace

到目前为止,我们摹拟了多个静态库的组织,和如何在主工程中援用他们。不过还存在1些小瑕疵,我截了 Xcode 中的1幅图:

Xcode 识别有问题

从图中可以很明显的发现: 第3方库中的代码被认为是系统代码,色彩为蓝色。而正常的自定义方法应当绿色,会对开发者造成困扰。

除这个小瑕疵之外,在之前谈到的跨项目依赖中,1个项目不单单需要援用另外一个项目的产物,还有1个先决条件: 把这两个项目放入同1个 Workspace 中。Workspace 的作用是组织多个 Project,使得各个 Project 直接可以有援用依赖关系,同时也能让 Xcode 辨认出各个 Project 中的代码和头文件。

按住 Command + Control + N 可以新建1个 Workspace:

新建 Workspace

完成以后就会看到1个完全空白的项目,在左边按下右键,选择 Add Files to:

添加文件

然后选中静态库项目和主工程的 .xcodeproj 文件,把这两个工程都加进来:

需要提示的是,切换到 Workspace 以后, Xcode 会把 Workspace 所在目录当作项目根目录,因此静态库的编译结果会放在 /CocoaPodsDemo/Build/Products/…,而不再是之前的 /CocoaPodsDemo/BSStaticLibraryOne/Build/Products/…,因此需要手动对主工程中的搜索路径做1下调剂。

做好上述改动后,即便我们删除掉 BSStaticLibraryOne 这个项目的编译结果,只在 Workspace 中编译主项目,Xcode 也会自动为我们编译被依赖的静态库。这就是为何我们只需要履行 pod install 下载好代码,就能够不用做别的操作,直接在主项目中运行。

固然,代码色彩毛病的小问题也在 Workspace 恢复正常了。

静态库嵌套

到这里,基本上关于 Cocoapods 的工作原理就算是分析完了。上述操作除文件增加,基本上都是修改 .pbxproj 文件。所有的 Xcode 都会在该文件中得到反应,同理,只要修改该文件,也能到达上述手动操作的效果。而 Cocoapods 开发了1套 Ruby 工具,用来封装这些修改,从而实现了自动化。

文章开头,我们提到作为代码提供者,如果自己的代码还援用别的第3方库,那末提供代码会变得很麻烦,这主要是由于静态库不会递归援用致使的。我们已知道静态库其实就是1堆编译好的目标文件(.o 文件)的打包情势,它需要配合头文件来使用。所谓的不会递归援用是指,假定项目 A 援用了静态库 B(或是动态库,也是1样),那末 A 编译后得到的静态库中,其实不含有静态库 B 的目标文件。如果有人拿到这样的静态库 A,就必须补齐静态库 B,否则就会遇到 “Undefined symbol” 毛病。

如果我们提供的代码援用了系统的动态库,问题还比较简单,只要在文档里面注明,让使用者自己导入便可。但如果是第3方代码,那末这简直是1起灾害。即便使用者找到了提供者使用的静态库,那个静态库也很有可能已进行了升级,而版本不1致的静态库可能具有完全不同的 API。也就是说代码提供者还要在文档中注明使用的静态库的版本,然后由使用者去找到这个版本。我想,这才是 Cocoapods 真正致力于解决的任务。

CocoaPods 的做法比较简单,由于他有1套统1的版本表示规则,也能够自动分析依赖关系,而且每一个版本的代码都有记录。后面会介绍 Cocoapods 的相干实践,这里我们先思考1下如何手动解决静态库嵌套的问题。

既然静态库只是目标文件的打包情势,那末我只需要找到被嵌套的静态库,拿到其中的目标文件,然后和外层的静态库放在1起重新打包便可。这个进程比较简单, 我也就没有做 demo,用代码应当就能够说明得很清楚。假定我们有静态库 A.a 和 B.a,其中 A 需要援用 B,现在我希望对外发布 A,并且集成 B:

lipo A.a -thin x86_64 output A_64.a # 如果是多 CPU 架构,先提取出某1种架构下的 .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 文件1起打包到 Together.a

这时候候 Together.a 文件就能够当作完全版的静态库 A 给他人使用了。

Cocoapods 使用

本来 Cocoapods 的使用就比较简单。特别是了解完原理后,使用起来应当更加得心应手了,对1些常见的毛病也有了分析能力。不过有个小细节还是需要注意1下:

Podfile.lock

关于 Cocoapods 文件是不是要加入版本控制并没有明确的答案。我之前的习惯是不加入版本控制。由于这样会让提交历史明显变得复杂,如果不同分支上使用的不同版本的 pod,在合并分支时就会出现大量冲突。

但是官方的推荐是把它加入到版本控制中去。这样他人不再需要履行 pod install,而且能够确保所有人的代码1定1致。

但是虽然不强迫把全部 Pod 都加入版本控制,但是 Podfile.lock 不管如何必须添加到版本控制系统中。为了解释这个问题,我们先来看看 Cocoapods 可能存在的问题。

假定我们在 Podfile 中写上: pod 'AFNetWorking',那末默许是安装 AFNetworking 的最新代码。这就致使用户 A 可能装的是 3.0 版本,而用户 B 再安装就变成了 4.0 版本。即便我们在 Podfile 中指定了库的具体版本,那也不能保证不出问题。由于1个第3方库还有可能依赖其他的第3方库,而且不保证它的依赖关系是具体到版本号的。

因此 Podfile.lock 存在的意义是将某1次 pod install 时使用的各个库的版本,和这个库依赖的其他第3方库的版本记录下来,以供他人使用。这样1来,pod install 的流程实际上是:

  1. 判断 Podfile.lock 是不是存在,如果不存在,依照 Podfile 中指定的版本安装
  2. 如果 Podfile.lock 存在,检查 Podfile 中每个 Pod 在 Podfile.lock 中是不是存在
  3. 如果存在, 则疏忽 Podfile 中的配置,使用 Podfile.lock 中的配置(实际上就是甚么都不做)
  4. 如果不存在,则使用 Podfile 中的配置,并写入 Podfile.lock 中

而另外一个经常使用命令 pod update 其实不是1个平常更新命令。它的原理是疏忽 Podfile.lock 文件,完全使用 Podfile 中的配置,并且更新 Podfile.lock。1旦决定使用 pod update,就必须所有团队成员1起更新。因此在使用 update 前请务必了解其背后产生的事情和对团队酿成的影响,并且确保有必要这么做。

发布自己的 Pod

很多教程都有介绍开源 Pod 的流程,我在实践的时候主要参考了以下两篇文章。相对来讲比较详细,条理清晰,也推荐给大家:

  1. Cocoapods系列教程(2)——开源主义接班人
  2. Cocoapods系列教程(3)——私有库管理和模块化管理

如果要创建公司内部的私有库,首先要建立1个自己的仓库,这个仓库在本地也会有存储:

仓库

如图中所示,master 是官方仓库,而 baidu 则是我用来测试的私有仓库。仓库中会存有所有 Pod 的信息,每一个文件夹下都依照版本号做了辨别,每一个版本对应1个 podspec 文件。从图中可以看到,cocoapods 会缓存所有的 podspec 到本地,但不会缓存每一个 Pod 的具体代码。每当我们履行 pod install 时,都会先从本地查找 podspec 缓存是不是存在,如果不存在则会去中央仓库下载。

我们常常遇到的 pod install 很慢就是由于默许情况下会更新全部 master。此时 master 不单单存储着本地使用 Pod 的 PodSpec 文件,而是存储了所有的已有的 Pod。所以这个更新进程看起来异常缓慢。有些解决方案是使用:

pod install --verbose --no-repo-update

这实际上是治标不治本的迁就医治方法,由于本地的仓库早晚要被更新,否则就拿不到最新的 PodSpec。要想完全解决这1问题,除定期更新外,还可以选择其他速度较快的镜像仓库。

podspec 文件是我们开源 Pod 时需要填写的文件,主要是描写了 Pod 的基础信息。除1些无关紧要的配置和介绍信息外,最重要的填写 source_filesdependency。前者用来规定哪些文件会对外公布,后者则指定此 Pod 依赖于哪些其他 Pod。比如在上图中,我的 PrivatePod 就依赖于 CorePod,在公司内部的项目中使用 PodS 依赖可以大量简化代码的集成流程。1个典型的 PodSpec 可能长这样:

填写好上述信息后,我们只要先 lint 1下 podspec,确保格式无误,就能够提交了。

生活不易,码农辛苦
如果您觉得本网站对您的学习有所帮助,可以手机扫描二维码进行捐赠
程序员人生
------分隔线----------------------------
分享到:
------分隔线----------------------------
关闭
程序员人生