作为一名开发者, 肯定对 Xcode 的项目文件.xcodeproj不陌生了. 我们用 Xcode 创建的任何一个项目都包含它. 那么大家是否对它有过进一步的了解呢. 我们来一探究竟.
.xcodeproj
首先.xcodeproj不是一个文件, 你可以理解为它是一个文件夹, 通过右键点击, 选择 "显示包内容" 来打开它:
之后, 会看到这几个文件:
其中project.pbxproj是我们要关注的主要内容. Xcode 项目的整体文件结构,以及编译,构建的配置信息,都保存在这个文件里. 打开这个文件你会看到类似这样的内容:
是不是顿时觉得不知所云呢, 几乎不在我们任何已认知的文件格式中. 这里只截取了一部分, 整个文件的结构都和上图类似.
没关系, 让我用尽量简单的介绍, 帮助大家理解它.
版本控制工具的阻碍
如果你的 Xcode 项目使用过多人协作的版本控制工具的话, 比如git. 那么你可能就会遇到过类似这样的问题, 团队的几个人同时开发一个项目, 其他人将自己的改动提交到版本库, 然后你拉取他们的提交, 更新到本地.
本来一切代码应该按照预期正常合并, 结果你发现拉取之后, 整个项目打不开了. 就像这样:
-
合并之前:
-
合并之后:
如果对这块细节了解不多的开发者, 很可能就会找另外一个提交代码的开发者当面沟通, 手动处理项目文件的改动. 或许这样可以解决问题, 但是效率并不高.
如果多想一步的人, 可能想到这可能是 XCode 项目文件合并冲突导致的. 实际上确实是这个原因. 但是当他们打开这个project.pbxproj文件后, 多数都会被它奇怪的格式阻拦.
下面就和大家聊聊project.pbxproj文件的数据格式, 了解它后, 不但能帮你解决上述这个问题, 还能让你更加深入了解 Xcode 项目的细节, 在一些其他场景也会有帮助.
project.pbxproj 文件格式
虽然一眼看去,project.pbxproj文件很凌乱, 但其实它的基本元素并不复杂, 下面是它的基础结构:
// !!
{
archiveVersion= 1;
classes= {
};
objectVersion= 45;
objects= {
...
};
rootObject= 0867D690FE84028FC02AAC07 /* Project object */;
}
其中,archiveVersion和objectVersion都是文件兼容版本号, 只需要按照 Xcode 生成的即可. 最重要的是objects属性. 这里面包含了我们项目中引用的所有文件, 配置等条目的信息.
rootObject是根对象的, 它的值是一串这样的内容0867D690FE84028FC02AAC07. 这个值是 Xcode 随机生成的, 我们自己也可以用任何算法生成它, 只需要保证它在整个文件中是唯一的. 类似这样一长串的值, 在整个项目文件中随处可见. Xcode 构建工具分析项目文件, 也是从rootObject开始逐级往下找的.
任何的project.pbxproj文件, 都在这个基础结构上生成, 你可以打开你自己的项目文件, 就可以看到这几个属性了.
了解每个条目
知道这个基础结构后, 我给大家展示一个实际的例子. 比如我们有一个项目文件, 它的rootObject如下:
rootObject= 1093A36C21F871FD00E71BC6
我们知道了根对象的 ID, 那么我们就搜索这个ID -1093A36C21F871FD00E71BC6. 你会找到这样一段内容:
/* Begin PBXProject section */
1093A36C 21F871FD00E 71BC6 /* Project object */= {
isa = PBXProject ;
attributes = {
LastSwiftUpdateCheck = 1010;
LastUpgradeCheck = 1010;
ORGANIZATIONNAME = swiftcafe;
TargetAttributes = {
1093A 37321F871FD00E 71BC6 = {
CreatedOnToolsVersion = 10. 1;
} ;
} ;
} ;
buildConfigurationList = 1093A 36F21F871FD00E 71BC6 /* Build configuration list for PBXProject "test" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en ;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
) ;
mainGroup = 1093A 36B21F871FD00E 71BC6 ;
productRefGroup = 1093A 37521F871FD00E 71BC6 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
1093A 37321F871FD00E 71BC6 /* test */,
) ;
} ;
/* End PBXProject section */
这段内容中,/* /包裹起来的内容是生成的注释, 帮助我们更容易理解这段内容. 比如/ Begin PBXProject section /和/ End PBXProject section */表示整个项目描述信息的开始和结束.
如果去掉注释后, 可以帮助你更清楚的理解这个文件的格式:
1093A36C 21F871FD00E 71BC6 = {
isa = PBXProject ;
attributes = {
LastSwiftUpdateCheck = 1010;
LastUpgradeCheck = 1010;
ORGANIZATIONNAME = swiftcafe;
TargetAttributes = {
1093A 37321F871FD00E 71BC6 = {
CreatedOnToolsVersion = 10. 1;
} ;
} ;
} ;
buildConfigurationList = 1093A 36F21F871FD00E 71BC6 ;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en ;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
) ;
mainGroup = 1093A 36B21F871FD00E 71BC6 ;
productRefGroup = 1093A 37521F871FD00E 71BC6 ;
projectDirPath = "";
projectRoot = "";
targets = (
1093A 37321F871FD00E 71BC6,
) ;
} ;
首先, 开头的1093A36C21F871FD00E71BC6是我们刚才在rootObject中看到的对应 ID, 是我们项目根对象. 再往下看isa是这个对象的类型 -PBXProject. 表示这个节点描述的是项目的基础信息.
比如我们可以看到ORGANIZATIONNAME是我们创先项目时候填入的组织名,CreatedOnToolsVersion是创建这个项目的 Xcode 版本, 等等.
有两个属性需要说明一下, 就是buildConfigurationList和mainGroup. 大家可以看到, 这两个属性的值,本身又是一个 ID. 这两个属性分别表示项目的配置列表, 以及主文件 Group. 它们本身都会包含很多子属性. 所以我们这里引用的是它们的 ID.
理解文件结构
你可以理解为 Xcode 项目文件, 所有的内容都是一个节点, 从根节点,一直向下引用的每一个子节点. 每个节点都包含一个 ID. 我们常见的 JSON 或 XML 结构也是树形结构, 但他们是比较直观的, 我们视觉上直接就能看到他们的父子节点关系. 而.pbxproj所有的节点都平铺在文件中, 视觉上不能一眼就看出整个的树形结构, 而他们之间是通过 ID 互相引用的. 理解这点至关重要.
举个例子, 如果我们的项目文件用 JSON 格式表示, 大概是这样:
{
rootObject: {
isa: "PBXProject",
mainGroup: {
isa: "PBXGroup",
children: [
{
isa : "PBXGroup",
path: "test"
children: [
...
]
}
]
}
}
}
同样的结构, 换成.pbxproj就是这样:
{
...
objects = {
1093A36C 21F871FD00E 71BC6 /* Project object */= {
isa = PBXProject ;
...
mainGroup = 1093A 36B21F871FD00E 71BC6 ;
...
} ;
1093A 36B21F871FD00E 71BC6 = {
isa = PBXGroup ;
children = (
1093A 37621F871FD00E 71BC6 /* test */,
) ;
sourceTree = "";
} ;
1093A 37621F871FD00E 71BC6 /* test */= {
isa = PBXGroup ;
children = (
...
) ;
path = test ;
sourceTree = "";
} ;
}
rootObject = 1093A36C 21F871FD00E 71BC6 /* Project object */;
}
可以看到,.pbxproj中所有节点, 都平铺在objects属性中. 通过每个节点的 ID, 互相引用, 建立起的树形结构.
节点类型
我们前面已经了解了一种节点的类型, 就是PBXProject项目基础信息..pbxproj还包含了很多类型的节点. 下面, 继续我们前面的探索.
我们前面看到mainGroup = 1093A36B21F871FD00E71BC6引用了另外一个节点, 通过搜索我们找到了这个节点的定义:
1093A 36B21F871FD00E 71BC6 = {
isa = PBXGroup ;
children = (
1093A 37621F871FD00E 71BC6 /* test */,
1093A 37521F871FD00E 71BC6 /* Products */,
) ;
sourceTree = "";
} ;
这个节点的类型是PBXGroup, 在它的children属性中, 又引用了另外两个节点. 并且 Xcode 在后面生成了注释/* test /和/ Products */, 其实就是这个mainGroup下级的两个子group. 对应我们 xcode 左边文件结构:
现在应该知道 Xcode 左边栏的文件结构是如何在项目文件中表示的了. 这里要提醒大家一点,/* test /和/ Products */这样的内容只是注释. 并不是配置文件的实际内容.
比如1093A37621F871FD00E71BC6 /* test */, 在配置文件中, 实际有效的是前半部分的 ID, 我们搜索这个ID, 会发现这个定义:
1093A37621F871FD00E71BC6 /* test */= {
isa = PBXGroup;
children = (
1093A37721F871FD00E71BC6 /* AppDelegate.swift */,
1093A37921F871FD00E71BC6 /* ViewController.swift */,
1093A37B21F871FD00E71BC6 /* Main.storyboard */,
1093A37E21F871FE00E71BC6 /* Assets.xcassets */,
1093A38021F871FE00E71BC6 /* LaunchScreen.storyboard */,
1093A38321F871FE00E71BC6 /* Info.plist */,
);
path = test;
sourceTree = "";
};
这个节点实际的文件路径, 其实是定义在它的path属性中. 每个 ID 后面的注释, 其实只是帮助我们更好的阅读配置文件的. 可以看到, 这个节点本身又引用了一些其他节点.
1093A37721F871FD00E71BC6 /* AppDelegate.swift */,
1093A37921F871FD00E71BC6 /* ViewController.swift */,
1093A37B21F871FD00E71BC6 /* Main.storyboard */,
1093A37E21F871FE00E71BC6 /* Assets.xcassets */,
1093A38021F871FE00E71BC6 /* LaunchScreen.storyboard */,
1093A38321F871FE00E71BC6 /* Info.plist */
他们又都对应了相应的文件, 比如1093A37721F871FD00E71BC6 /* AppDelegate.swift */, 你搜索它的 ID , 就会找到关于这个文件的明确定义:
1093A37721F871FD00E71BC6 /* AppDelegate.swift */= { isa= PBXFileReference; lastKnownFileType= sourcecode.swift; path= AppDelegate.swift; sourceTree= ""; };
这种文件节点, 是树形结构的最末端阶段, 它在配置文件中直接写成了一行, 我们把它的格式整理一下, 这样就好理解了:
1093A 37721F871FD00E 71BC6 /* AppDelegate.swift */= {
isa = PBXFileReference ;
lastKnownFileType = sourcecode. swift;
path = AppDelegate. swift;
sourceTree = "";
} ;
这个节点的类型是PBXFileReference, 顾名思义是文件引用. 同样, 它还包含了一些其他属性, 比如文件的路径, 类型等.
结尾
通过上面咱们一起的梳理, 你应该不难发现了,.pbxproj文件结构其实和我们常见的树形结构没什么两样, 只是他不像类似 JSON 这样的数据结构看起来那么直观. 每个节点的定义在文件中看起是来同级的, 但他们又通过 ID 的互相引用实现了逻辑上的树形结构.
只要你了解了这个概念, 你下次遇到比如合并冲突,或者其他需要操作.pbxproj文件的问题, 运用这个原理基本都可以解决.
如果你想了解.pbxproj文件都有多少种节点类型, 以及每种类型的详细定义, 这里给大家推荐一个地址, 有比较全面的介绍www.monobjc.net/xcode-proje….
另外在补充一下, 我们前面提到的节点 ID, 比如这个1093A37721F871FD00E71BC6 /* AppDelegate.swift */, 一长串16进制数..pbxproj并没有规定节点ID固定的生成算法. 我们看到的这串内容,实际上是 Xcode 用自身的算法生成的. 但其实一个.pbxproj文件, 它里面的节点ID 可以用任何算法生成, 只要保证他在当前文件中全局唯一不重复即可.