引子
在「Molinillo 依赖校验」通过后,CocoaPods 会根据确定的 PodSpec 下载对应的源代码和资源,并为每个 PodSpec 生成对应的 Xcode Target。本文重点就来聊聊 Xcode Project 的内容构成,以及 xcodeproj[1] 是如何组织 Xcode Project 内容的。
Xcode 工程文件
早在前文「Podfile 的解析逻辑」中,我们简单介绍过 Xcode 的工程结构:Workspace、Project、Target 及 Build Setting 等。
我们先来了解下这些数据在 Xcode 中是如何表示,了解这些结构才能方便我们理解 xcodeproj 的代码设计思路。
xcworkspace
早在 Xcode 4 之前就出现 workspace bundle 了,只是那会 workspace 仍内嵌于 .xcodeproj
中。Xcode 4 之后,我们才对 workspace 单独可见。让我们新建一个 Test.xcodeproj 项目,来看看其目录结构:
Test.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcuserdata
│ └── edmond.xcuserdatad
│ └── UserInterfaceState.xcuserstate
└── xcuserdata
└── edmond.xcuserdatad
└── xcschemes
└── xcschememanagement.plist
可以发现 Test.xcodeproj
bundle 内包含 project.workspace
。而当我们通过 pod install
命令成功添加 Pod 依赖后,Xcode 工程目录下会多出 Test.workspace
,它是 Xcodeproj 替我们生成的,用于管理当前的 Test.project
与 Pods.pbxproj
。新建的 workspace 目录如下:
Test.xcworkspace
└── contents.xcworkspacedata
生成的 workspace 文件夹内部只包含了 contents.xcworkspacedata
,为 xml 格式的内容:
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Test.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>
在标签 Workspace 下声明了两个 FileRef
其地址分别指向了 Test.xcodeproj
和 Pods.xcodeproj
。这里注意的是 FileRef
属性的值使用前缀 group + path
来修饰的,而内嵌的 project.xcworkspace
,使用 self
作为前缀:
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
另外,当我们用 Xcode 打开项目后,能发现 workspace 目录下会自动生成 xcuserdata
目录,它用于保存用户的 Xcode 配置。比如:
UserInterfaceState.xcuserstate
:以二进制的 Plist 保存,用于记录窗口布局等个性化设置。xcdebugger
:记录各种断点数据。
这也是在日常开发中,经常会选择将 xcuserdata
目录 ignore 掉的原因。
xcodeproj
与 .xcworkspace
类似 .xcodeproj
同为 Xcode 工程配置的 bundle,接下来重点展开 project.pbxproj
,它记录了 Xcode 工程的各项配置,本质上是一种旧风格的 Plist 文件。
Property List
Plist 被设计为人类可读的、可以手工修改的格式,故采用了类似于编程语言的语法将数据序列化为ASCII数据。Plist 最早可追溯到 NeXTSTEP 时期,由于历史原因,目前它支持多种格式,string、binary、array、dictionary 等类型数据。相比于 JSON,Plist 还支持二进制数据的表示,以 <>
修饰文本形式的十六进制数,其中字典与数组的区别如下:
Array:
plist => ( "1", "2", "3" )
json => [ "1", "2", "3" ]
Dictionary:
plist => { "key" = "value"; ... }
json => { "key" : "value", ... }
处理 Plist 文件可使用 Unix 提供的 plutil
工具。比如将 Plist 文件转成 XML 格式:
plutil -convert xml1 -s -r -o project.pbxproj.xml project.pbxproj
-convert fmt
选项支持转换的格式有:xml1、binary1、json、swift、json。
project.pbxproj
pbxproj 文件全称为 Project Builder Xcode Project,光看第一层元数据比较简单:
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objects = {
...
};
rootObject = 8528A0D92651F281005BEBA5 /* Project object */;
}
文件以明确的编码信息开头,archiveVersion
通常为 1,表示序列化的版本号;classes
则似乎一直为空;objectVersion
表示所兼容的最低版本的 Xcode,该数字与 Xcode 的版本对应关系如下:
53 => 'Xcode 11.4',
52 => 'Xcode 11.0',
51 => 'Xcode 10.0',
50 => 'Xcode 9.3',
48 => 'Xcode 8.0',
47 => 'Xcode 6.3',
46 => 'Xcode 3.2',
45 => 'Xcode 3.1',
rootObject
记录的 16 进制数字,为 project 对象的索引。这里我们可以称其为 Xcode Object Identifier,pbxproj 中的每个 Xcode Object 创建时,都会生成对应唯一标识数字,而上面的 objects
字典则记录了整个 Xcode 项目的所有 Xcode Object。
Xcode Object Identifiers
Xcode Object Identifier 是用 24 位的 16 进制字符表示,我们暂且称其为 GUID。
⚠️ 注意这并不意味着它与其他称为 GUID 的其他事物相似。
生成的 GUID 不仅在项目文件中必须唯一,并且在 Xcode 中同时打开的其他项目文件中也必须唯一,即跨工程唯一性。只有这样能避免了多人合作中,同时新增或编辑工程文件带来的问题。这其实是一个有趣的竞争需求,有兴趣的可以查看 Premake[2] 这个项目,它能保证重新生成的项目具有相同的 GUID。
对于 Xc