Xcode工程
一个常见的Xcode的工程如下:
可以看到我们经常面对的一些地方,Workspace,Project,Scheme,Target
Target
Target是最小的编译单元,产物可在Product目录下看到,target可以为多种类型,比如framwork,extension,application等,每一个target对应着一个product产物,且可以独立配置。
Target配置
对于每一个Target,都有自己的独立配置,如上图中绿框所示,分别是
- General:配置基础的信息,如Product的名字,bundle ID等信息。
- Signing & Capailities:签名,能力(如推送能力)配置
- Resource Tags:按需加载资源配置
- Info:info文件配置,如权限配置等
- Build Settings:配置Target,如指定使用的编译器,目标平台、编译参数、头文件搜索路径等
- Build Phases:build阶段配置,如前置依赖、执行的脚本文件
- Build Rules:配置自定义构建规则
要新建一个Target也很容易,在菜单栏点击File-> New ->Target或者点击TARGETS栏底部"+"即可,就会弹出要新建什么类型的Target,创建成功后,可以在Products文件夹中看到对应的Product。
Target依赖
Target之间也可以有依赖,我们通过Cocoapods倒入进来的依赖库或者手动创建的Target都会自动添加到主Target的依赖中,需要注意的一点是通过cocoapods导入的依赖最终会被依赖为Pods_主工程名.framework
:
Project
Project是Target的载体,也是Xcode可以直接打开的工程。Project无法被编译,所以对于一个Project而言,至少包含一个Target。
Project还可以包含其他的Project。
可以看出,一个Project可以包含多个Target。其中一个Project还有Build Settings等配置。如果Target中的Build Settings有相同的配置,则Target中的配置会继承或覆盖Project的配置。
Workspace
Workspace就是Project容器。一个WorkSpace可以装载多个Project。当我们打开一个WorkSpace的时候,WorkSpace中的Project是相互可见的。 对xxx.xcworkspace文件单击右键,显示包内容,如下:
可以理解为它们之间的关系如下:
Scheme
Scheme是一个理解为一个构建流程。定义了构建的Target,构建配置,以及测试配置。每一次构建,只能选择一个Scheme。点击下图位置即可配置和新建Scheme。
每一个Scheme都会对应一个Target。指明Target的各个构建流程的配置是怎样的,包括了Build、Run、Test、Profile、Analyze、Archive等操作每一个过程都可以单独配置。如下:
Settings
Build Setting是一个构建变量,指定了Target在构建中的信息。如指定Xcode传给编译器的变量。
除了上面在Project,Target中的Build Settings,我们也可以去自定义一个Build Settings。在Xcode工程中点击
File-> New -> File -> Configuration Settings File
或者cmd+n
选择Configuration文件。
在添加xcconfig文件的时候我们需要设置添加到哪个Target
Xcode工程在构建过程中,会按以下的顺序读取配置
- .xcconfig文件中的配置
- Target的Build Settings
- Project的Build Settings
- 平台的默认值
Cocoapods
Ruby工具链
CocoaPods其实是一个基于Ruby实现的库管理工具。先介绍一下Ruby常用的开发环境。
Ruby:一种开发语言,类似于JAVA,Python等
RVM:用于帮你安装Ruby环境,帮你管理多个Ruby环境,帮你管理你开发的每个Ruby应用使用机器上哪个Ruby环境
RubyGems是一个Ruby程序包管理器。它将一个Ruby应用程序打包到一个gem里,作为一个安装单元。
Gem:是封装起来的Ruby应用程序或代码库。
Gemfile:定义你的应用依赖哪些第三方包,bundle根据该配置去寻找这些包。
Bundler:是管理 Gem 依赖的工具。在配置文件Gemfile里说明你的应用依赖哪些第三方包,他自动帮你下载安装多个包,并且会下载这些包依赖的包
对于CocoaPods,其实也是一个Gem。所以我们可以通过添加一个Gemfile文件为项目指定CocoaPods版本。
CocoaPods也借鉴了这种模式。结合上方设计如下:
CocoaPods架构
CocoaPods其实是一个架构设计清晰的框架,将功能模块一个个划分。概览设计如下:
-
CALide:负责处理在终端输入的命令,如pod init,将终端命令转换成需要执行的ruby代码
-
CocoaPods-core: 负责解析DSL模版,也就是我们的Podfile,.podSpec文件。我们的Podfile文件中编写的内容其实是Ruby,可以通过eval特性将Podfile中字符串解析成Ruby代码。
-
CocoaPods-Downloader: 负责下载源码。经过解析Podfile后中得到Ruby代码,会将每一个依赖的存入到数组,然后把这些代码下载下来
我们可以通过pod init
帮忙生成一个Podfile文件,配置依赖后运行pod install
会生成一个worksapce文件和一个Pods文件夹,其中包含一个名为Pods的Project:
-
XcodeProj: 负责操作Xcode工程。下载完代码以后生成Pods工程和WorkSpace,为依赖的库生成target,并根据库与库之间的关系为Target添加依赖。
-
Cocoapods-plugins: CocoaPods中部分功能以plugin的形式提供,可以通过 pod plugins installed获取已经安装的plugin。如下
目录的组成部分:
- 1、Pods.xcodeproj,Pods库的工程;每个Pod库会对应其中某个target,每个target都会打包出来一个.a文件;
- 2、依赖库的文件目录;以SDWebImage为例,会有个SDWebImage目录存放文件;
- 3、manifest.lock,Pods目录中的Pod库版本信息;每次pod install的时候会检查manifest.lock和Podfile.lock的版本是否一致,不一致的则会更新;
- 4、Target Support Files、Headers、Local Podspecs目录等;Target Support Files里面是一些target的工程设置xcconifg以及脚本等,Headers里面有Public和Private的头文件目录,Local Podspecs是存放从本地Pod库(:path或者:podspec指定时)install时的podspec
一些重要的文件:
1.Podfile.lock
pod install会解析依赖并生成Podfile.lock文件;如果Podfile.lock存在时执行pod install,则不会修改已经install的pod库。(注意,pod update则会忽视Podfile.lock进行依赖解析,最后重新install所有的Pod库,生成新的Podfile.lock)
在多人开发的项目中,Pods目录由于体积较大,往往不会放在Git仓库中,Podfile.lock文件则建议添加到Git仓库。当其他人修改Podfile时,pod install生成新的Podfile.lock文件也会同步到Git。这样能保证拉下来的版本库是其他人一致的。
在Xcode中可以看到如下:
每一个通过pod引入的库,都会生成对应的Target,每个Target的产物是framework,如果没有标记use_frameworks
,pod会生成.a产物。
pod install的时候,Pods目录下生成一个Manifest.lock文件,内容与.lock文件完全一致;在每次build工程的时候,会检查这两个文件是否一致。
2.PodSpec
在每个Pod库的仓库中,都会有一个podspec文件,描述Pod库的版本、依赖等信息:
可以在podspec中指定当前库的依赖和依赖的版本
Pod库依赖解析
CocoaPod的依赖管理相对第三方库手动管理更加便捷。
在手动管理第三方库中,如果库A集成了库F,库B也集成了库F ,就会遇到库F符号冲突的问题,需要将库A/B和库F的代码分开,手动添加库F;后续如果库A/B版本有更新,也需要手动去处理。
而在CocoaPod依赖解析中,可以把每个Pod库都看成一个节点,Pod库的依赖是它的子节点; 依赖解析的过程,就是在一个有向图中找到一个拓扑序列。
一个合法的Podfile描述的应该是一个有向无环图,可以通过拓扑排序的方式,得到一个AOV网。
按照这个拓扑序列中的顶点次序,可以依次install所有的Pod库并且保证其依赖的库已经install。
有时候会陷入循环依赖的怪圈,就是因为在有向图中出现环,则无法通过算法得到一个拓扑排序。
Pods工程和主工程的关系
在实际的开发过程,容易知道Pods工程是先编译,编译完再执行主工程的编译,因为主工程的Linked Libraries里面有Pods-testLibs.framework。
那么Pod库中的target编译顺序是如何决定?
打开workspace,选择Pods工程。从上图分析我们知道,主工程最终需要的是Pods-testLibs.framework。看看Pods-testLibs的Build Phases选项,从target依赖中可以看到其他三个target。
分析至此,我们可以知道这里的编译顺序是SnapKit、Kingfisher、FluentIcons、Pods-testLibs、testLibs(主工程target)。
接下来我们分析编译过程。SnapKit因为没有依赖,所以编译的时候header search path为空。
再来看下Pods-testLibs,他有三个依赖,分别是SnapKit,FluentIcons,Kingfisher,所以在header search paths中需要设置这三个库的头文件路径
编译的结果有4个frameworks(FluentIcons.framework,Kingfisher.framework,SnapKit.framework,Pods-testLib.framework),我们在看下Pod-testLib.framework中二进制文件的大小:
从Pods-testLib的大小,我们可以知道Pods-testLib不是多个二进制的集合,仅仅是作为主工程的一个依赖,使得Pod库工程能先于主工程编译。
那么,主工程编译的时候如何去找到Kingfisher的头文件和二进制文件?
从主工程的Search Paths我们可以看到,Framewok是有说明具体的位置;
这些信息是CocoaPod生成的一份xcconfig,里面的HEADER_SEARCH_PATHS和LIBRARY_SEARCH_PATHS会指明这两个地址
对于资源文件,CocoaPods 提供了一个名为 Pods-resources.sh 的 bash 脚本,该脚本在每次项目编译的时候都会执行,将第三方库的各种资源文件复制到目标目录中。
CocoaPods 通过一个名为 Pods.xcconfig 的文件来在编译时设置所有的依赖和参数。
在编译之前会检查pod的版本是否发生变化(manifest和.lock文件对比),以及执行一些自定义的脚本。
编译得到可执行文件后,会进行asset、storyboard等资源文件的处理,还会执行pod的脚本,把pod的资源复制过来。
全部准备就绪,就会生成符号表,包括二进制文件里面的符号。
最后进行签名、校验,得到.app文件