1. 背景
Xcode作为日常开发iOS程序的IDE,支持C、C++、Objective-C、Swift、Ruby等语言进行编写。日常开发入口就是Xcode workspace或者Xcode project。
workspace是一个Xcode文档,它将项目和其他文件、project分组。一个workspace可以包含任意数量的Xcode project,以及资源文件(JSON、脚本、图片、视频等)。workspace除了组织每个project中的文件外,还提供了所包含项目及其目标之间的隐式和显式关系。
project就是一个 Xcode 工程,它是实际管理工程下 targets 、源码、资源文件、framework 等。project 只是一个容器,本身是无法被编译的,所以每个 project 至少应该有一个可编译的 target, target下需要包含可编译的源码。
在日常开发中难免会去在非Xcode的环境下去操作workspace或者project中的依赖关系,所以搞懂workspace、project、源码和资源文件之间的关系就显得特别重要,因为知道怎么来的才知道去如何做。
2. 了解workspace全貌
![图片](/i/l/?n=23&i=blog/27422/202302/27422-20230220152407129-111695393.jpg)
由上图可以简单看出workspace和project的关系:
-
一个workspace里可以包含多个project
-
一个project里包含多个target
-
configuration 即Xcode中的Debug/Release 等工程配置
-
scheme 配置target编译参数
-
每个target即每次编译生成对应产物:app或者framework
3. 探寻workspace
新创建一个空的workspace,直接看他的层级树:
![图片](/i/l/?n=23&i=blog/27422/202302/27422-20230220152407483-1339152774.jpg)
可以看到workspace主要包含三个层级:
-
xcworkspacedata,workspace的配置文件,实际上就是一个XML文件。
-
xcshareddata:可共享的配置,包含scheme、script等信息。
-
xcuserdata:当前用的配置,包含本地scheme、script、断点信息等。
以报价项目为例详细查看 contents.xcworkspacedata的内容:
![图片](/i/l/?n=23&i=blog/27422/202302/27422-20230220152406902-2094334738.png)
FileRef 顾名思义它是标记了每个文件在workspace中的路径关系,这个关系决定的在Xcode中的project的展示层级。
location的关键字包含如下:
-
self:当前文件夹下的同名project
-
group:指定目录下的xcodeproj文件
-
container:workspace当前目录下的不同名的xcodeproj文件
-
absolute:绝对路径下的文件
4. 探寻project
由对workspace的探寻我们可以看到,workspace确实只是把project等文件组织起来的一个工作空间,本身并不具备对源码、资源的编译、整合能力,进一步探寻到project文件,我们才能看到源码、资源文件等是怎么被整合起来的。
![图片](/i/l/?n=23&i=blog/27422/202302/27422-20230220152407483-1608672534.jpg)
由上图可看到,xcodeproj里包含了三个大的层级,xcuserdata里包含的常用的scheme和配置文件,还包含了一个xcodeworkspace,这是为了保证Xcode的兼容性,维持Xcode管理文件逻辑的统一。
xcodeproj中包含了开发中所需要的全部文件,管理了当前工程所有的源码、资源文件、配置文件等。重点是pbxproj文件,它与我们正常编译代码密切相关,管理target、文件之间的引用依赖关系、合并代码时候产生的文件冲突就在这里。
4.1
深入pbxproj
pbxproj全拼是Project Builder Xcode Project,它其实是我们熟悉的plist文件的一种,但是它不像我们常用的plist文件有着优越的可读性,由于历史原因它才被Xcode一直保存下来。
pbxproj中定义了target、script、文件、configuration等之间的引用关系,我们看到的Xcode项目布局实际上是可视化了pbxproj。
直接看看pbxproj的内部布局吧:
![图片](/i/l/?n=23&i=blog/27422/202302/27422-20230220152406483-1615489522.png)
可以看到,最外层包含了这些属性:
-
archiveVersion 当前文件版本
-
classes 占位符
-
objectVersion 当前文件需要的 Xcode最低版本
-
objects 以每个object的uuid为key的字典,存放了object属性
-
rootObject 当前文件的根object (isa = PBXProject)
objects里实际上存放的就是每个文件之间的依赖关系,我们称每个文件是一个Xcode object,这个Xcode object不仅仅可以是源码文件,也可以是group、framework、app、target、scheme等。
由上图的rootObject = D9658FA7290BA51D00A72187,我们简单看一下它作为Xcode object的内部结构:
![图片](/i/l/?n=23&i=blog/27422/202302/27422-20230220152406536-104813380.png)
可以看到比较重要的信息是isa、mainGroup、configration、target,其他信息也都包含了Xcode中我们见到的、可以配置的全部信息。
这只是PBXProject中的信息,全部信息可在官网进行查询。下面列出了所有的类型配置:
-
PBXProject:Project 配置,编译工程所需信息
-
PBXNativeTarget:Target 的配置
-
PBXTargetDependency:Target 依赖关系配置
-
PBXContainerItemProxy:部署的元素
-
XCConfigurationList:Xcode中configuration配置
-
XCBuildConfiguration:Xcode 的 Build Settings 配置
-
PBXVariantGroup:storyboard 文件配置
-
PBXBuildFile:各类文件配置
-
PBXFileReference:各类文件引用配置
-
PBXGroup:Xcode中的group
-
PBXSourcesBuildPhase:需要编译的编译源文件
-
PBXFrameworksBuildPhase:需要编译的framework
-
PBXResourcesBuildPhase:除源码外的资源文件
他们之间的关系大致如下
![图片](/i/l/?n=23&i=blog/27422/202302/27422-20230220152406588-909404478.png)
了解了各个文件之间的关系,可以为我们以后通过脚本去动态添加、删除、移动文件、修改build settings、scheme等操作打下基础。
5. 探寻scheme
scheme不是编译target的必要条件,没有scheme不影响Xcode的编译操作,但是,没有scheme我们就没办法在编译时传入参数条件,插入编译脚本,配置个性化编译配置,所以scheme是Xcode编译时的必须选项。
打开一个scheme源文件,我们可以看到如下布局:
![图片](/i/l/?n=23&i=blog/27422/202302/27422-20230220152407155-2083553580.png)
可以看到,最外层包含着build、test、launch、profile、analyze、archive。恰好对应了Xcode中的与之对应的命令,再次验证了Xcode就是pbxproj的可视化呈现。
进入BuildAction可以看到我们在Xcode中添加的预编译脚本和各种环境变量配置,这些配置有的是在编译过程中必不可少的参数,有的是方便我们管理编译产物的必须配置,灵活运用这些配置,可以让Xcode更好的为我们服务。
6. 探寻target
target用于指定要构建的产物,即framework或者app。target只包含了当前project中的部分指定的代码和资源文件,每一个target只能构建出一个特定的构建产物,为了丰富构建产物,一个project可以拥有多个target。
target使用Build Settings和Build Phases的形式来进行个性化配置,默认这些配置可以通过project继承,也可以通过手动或者配置文件的方式覆盖其他配置。
target之间可以互相依赖,如果是在同个workspace下,Xcode默认会触发隐式依赖,当然,如果用手动配置依赖关系,则会变为显式依赖。显式依赖的优先级高于隐式依赖。
7. 总结与展望
根据上面的介绍,大家一定对Xcode的工程配置有了一定的了解,在了解了这些之后,我们能做些什么呢?其实是有很多玩法的:
-
根据不同的编译scheme编译指令,提取出编译产物,分发给不同的人员。
-
根据target的不同,在不改变源码的前提下,每次编译设置不同的环境测试包。
-
编译过程中检查出警告信息及时上报开发人员。
-
编译时找出无效代码及文件。
-
利用cocoapods的动态配置在安装的时候直接引入二进制组件以增加编译速度。
-
输出指定framework的编译日志到文件方便对比查阅。
了解了这些基础配置,以后在项目的工程化方面才有更多手段解决重复度高或者棘手的问题,还有更多的新玩法可以在工作过程中发掘。
作者|王一飞