了解 Xcode 项目文件 .xcodeproj

swift 发布于 2019年06月24日

作为一名开发者, 肯定对 Xcode 的项目文件 .xcodeproj 不陌生了. 我们用 Xcode 创建的任何一个项目都包含它. 那么大家是否对它有过进一步的了解呢. 我们来一探究竟.

.xcodeproj

首先 .xcodeproj 不是一个文件, 你可以理解为它是一个文件夹, 通过右键点击, 选择 "显示包内容" 来打开它:

之后, 会看到这几个文件:

其中 project.pbxproj 是我们要关注的主要内容. Xcode 项目的整体文件结构,以及编译,构建的配置信息,都保存在这个文件里. 打开这个文件你会看到类似这样的内容:

是不是顿时觉得不知所云呢, 几乎不在我们任何已认知的文件格式中. 这里只截取了一部分, 整个文件的结构都和上图类似.

没关系, 让我用尽量简单的介绍, 帮助大家理解它.

版本控制工具的阻碍

如果你的 Xcode 项目使用过多人协作的版本控制工具的话, 比如 git. 那么你可能就会遇到过类似这样的问题, 团队的几个人同时开发一个项目, 其他人将自己的改动提交到版本库, 然后你拉取他们的提交, 更新到本地.

本来一切代码应该按照预期正常合并, 结果你发现拉取之后, 整个项目打不开了. 就像这样:

合并之前:

合并之后:

如果对这块细节了解不多的开发者, 很可能就会找另外一个提交代码的开发者当面沟通, 手动处理项目文件的改动. 或许这样可以解决问题, 但是效率并不高.

如果多想一步的人, 可能想到这可能是 XCode 项目文件合并冲突导致的. 实际上确实是这个原因. 但是当他们打开这个 project.pbxproj 文件后, 多数都会被它奇怪的格式阻拦.

下面就和大家聊聊 project.pbxproj 文件的数据格式, 了解它后, 不但能帮你解决上述这个问题, 还能让你更加深入了解 Xcode 项目的细节, 在一些其他场景也会有帮助.

project.pbxproj 文件格式

虽然一眼看去, project.pbxproj 文件很凌乱, 但其实它的基本元素并不复杂, 下面是它的基础结构:

// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 45;
objects = {

...

};
rootObject = 0867D690FE84028FC02AAC07 /* Project object */;
}

其中, archiveVersionobjectVersion 都是文件兼容版本号, 只需要按照 Xcode 生成的即可. 最重要的是 objects 属性. 这里面包含了我们项目中引用的所有文件, 配置等条目的信息.

rootObject 是根对象的, 它的值是一串这样的内容 0867D690FE84028FC02AAC07. 这个值是 Xcode 随机生成的, 我们自己也可以用任何算法生成它, 只需要保证它在整个文件中是唯一的. 类似这样一长串的值, 在整个项目文件中随处可见. Xcode 构建工具分析项目文件, 也是从 rootObject 开始逐级往下找的.

任何的 project.pbxproj 文件, 都在这个基础结构上生成, 你可以打开你自己的项目文件, 就可以看到这几个属性了.

了解每个条目

知道这个基础结构后, 我给大家展示一个实际的例子. 比如我们有一个项目文件, 它的 rootObject 如下:

rootObject = 1093A36C21F871FD00E71BC6

我们知道了根对象的 ID, 那么我们就搜索这个ID - 1093A36C21F871FD00E71BC6. 你会找到这样一段内容:

/* Begin PBXProject section */
1093A36C21F871FD00E71BC6 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1010;
LastUpgradeCheck = 1010;
ORGANIZATIONNAME = swiftcafe;
TargetAttributes = {
1093A37321F871FD00E71BC6 = {
CreatedOnToolsVersion = 10.1;
};
};
};
buildConfigurationList = 1093A36F21F871FD00E71BC6 /* Build configuration list for PBXProject "test" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 1093A36B21F871FD00E71BC6;
productRefGroup = 1093A37521F871FD00E71BC6 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
1093A37321F871FD00E71BC6 /* test */,
);
};
/* End PBXProject section */

这段内容中, /* */ 包裹起来的内容是生成的注释, 帮助我们更容易理解这段内容. 比如 /* Begin PBXProject section *//* End PBXProject section */ 表示整个项目描述信息的开始和结束.

如果去掉注释后, 可以帮助你更清楚的理解这个文件的格式:

1093A36C21F871FD00E71BC6 = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1010;
LastUpgradeCheck = 1010;
ORGANIZATIONNAME = swiftcafe;
TargetAttributes = {
1093A37321F871FD00E71BC6 = {
CreatedOnToolsVersion = 10.1;
};
};
};
buildConfigurationList = 1093A36F21F871FD00E71BC6;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 1093A36B21F871FD00E71BC6;
productRefGroup = 1093A37521F871FD00E71BC6;
projectDirPath = "";
projectRoot = "";
targets = (
1093A37321F871FD00E71BC6,
);
};

首先, 开头的 1093A36C21F871FD00E71BC6 是我们刚才在 rootObject 中看到的对应 ID, 是我们项目根对象. 再往下看 isa 是这个对象的类型 - PBXProject. 表示这个节点描述的是项目的基础信息.

比如我们可以看到 ORGANIZATIONNAME 是我们创先项目时候填入的组织名, CreatedOnToolsVersion 是创建这个项目的 Xcode 版本, 等等.

有两个属性需要说明一下, 就是 buildConfigurationListmainGroup. 大家可以看到, 这两个属性的值,本身又是一个 ID. 这两个属性分别表示项目的配置列表, 以及主文件 Group. 它们本身都会包含很多子属性. 所以我们这里引用的是它们的 ID.

理解文件结构

你可以理解为 Xcode 项目文件, 所有的内容都是一个节点, 从根节点,一直向下引用的每一个子节点. 每个节点都包含一个 ID. 我们常见的 JSON 或 XML 结构也是树形结构, 但他们是比较直观的, 我们视觉上直接就能看到他们的父子节点关系. 而 .pbxproj 所有的节点都平铺在文件中, 视觉上不能一眼就看出整个的树形结构, 而他们之间是通过 ID 互相引用的. 理解这点至关重要.

举个例子, 如果我们的项目文件用 JSON 格式表示, 大概是这样:

{
rootObject: {
isa: "PBXProject",
mainGroup: {
isa: "PBXGroup",
children: [
{
isa : "PBXGroup",
path: "test"
children: [
...
]
}
]
}
}
}

同样的结构, 换成 .pbxproj 就是这样:

{
...
objects = {
1093A36C21F871FD00E71BC6 /* Project object */ = {
isa = PBXProject;
...
mainGroup = 1093A36B21F871FD00E71BC6;
...
};
1093A36B21F871FD00E71BC6 = {
isa = PBXGroup;
children = (
1093A37621F871FD00E71BC6 /* test */,
);
sourceTree = "<group>";
};
1093A37621F871FD00E71BC6 /* test */ = {
isa = PBXGroup;
children = (
...
);
path = test;
sourceTree = "<group>";
};
}
rootObject = 1093A36C21F871FD00E71BC6 /* Project object */;
}

可以看到, .pbxproj 中所有节点, 都平铺在 objects 属性中. 通过每个节点的 ID, 互相引用, 建立起的树形结构.

节点类型

我们前面已经了解了一种节点的类型, 就是 PBXProject 项目基础信息. .pbxproj 还包含了很多类型的节点. 下面, 继续我们前面的探索.

我们前面看到 mainGroup = 1093A36B21F871FD00E71BC6 引用了另外一个节点, 通过搜索我们找到了这个节点的定义:

1093A36B21F871FD00E71BC6 = {
isa = PBXGroup;
children = (
1093A37621F871FD00E71BC6 /* test */,
1093A37521F871FD00E71BC6 /* Products */,
);
sourceTree = "<group>";
};

这个节点的类型是 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 = "<group>";
};

这个节点实际的文件路径, 其实是定义在它的 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 = "<group>"; };

这种文件节点, 是树形结构的最末端阶段, 它在配置文件中直接写成了一行, 我们把它的格式整理一下, 这样就好理解了:

1093A37721F871FD00E71BC6 /* AppDelegate.swift */ = {
isa = PBXFileReference;
lastKnownFileType = sourcecode.swift;
path = AppDelegate.swift;
sourceTree = "<group>";
};

这个节点的类型是 PBXFileReference, 顾名思义是文件引用. 同样, 它还包含了一些其他属性, 比如文件的路径, 类型等.

结尾

通过上面咱们一起的梳理, 你应该不难发现了, .pbxproj 文件结构其实和我们常见的树形结构没什么两样, 只是他不像类似 JSON 这样的数据结构看起来那么直观. 每个节点的定义在文件中看起是来同级的, 但他们又通过 ID 的互相引用实现了逻辑上的树形结构.

只要你了解了这个概念, 你下次遇到比如合并冲突,或者其他需要操作 .pbxproj 文件的问题, 运用这个原理基本都可以解决.

如果你想了解 .pbxproj 文件都有多少种节点类型, 以及每种类型的详细定义, 这里给大家推荐一个地址, 有比较全面的介绍 http://www.monobjc.net/xcode-project-file-format.html.

另外在补充一下, 我们前面提到的节点 ID, 比如这个 1093A37721F871FD00E71BC6 /* AppDelegate.swift */, 一长串16进制数. .pbxproj 并没有规定节点ID固定的生成算法. 我们看到的这串内容,实际上是 Xcode 用自身的算法生成的. 但其实一个 .pbxproj 文件, 它里面的节点ID 可以用任何算法生成, 只要保证他在当前文件中全局唯一不重复即可.

我之前的文章曾经给大家介绍过一个工具 xUnique - 摆脱 XCode 的 project 文件冲突, 就是关于 ID 生成这个事情的. 你也不妨了解一下.


如果你觉得这篇文章有帮助,还可以关注微信公众号 swift-cafe,会有更多我的原创内容分享给你~

本站文章均为原创内容,如需转载请注明出处,谢谢。
关注微信公众号
发现更多精彩
swift-cafe