project.pbxproj 文件被包含于 Xcode 工程文件 *.xcodeproj 之中,存储着 Xcode 工程的各项配置参数。它本质上是一种旧风格的 Property List 文件,历史可追溯到 NeXT 的 OpenStep。其可读性不如 xml 和 json,苹果却一直沿用至今,作为一家以创新闻名的公司可能这里剩下的就是情怀吧。

本文谈了下 project.pbxproj 的知识,并总结了一些操作工程文件的优秀轮子,并在最后给出了自己的解决方案 pbxprojHelper (https://github.com/yulingtianxia/pbxprojHelper)。

Property List 的历史

想了解 project.pbxproj 文件格式,就需要先了解 Property List。

Property List 有很多种表现方式,最古老的格式就是之前提到的 NeXTSTEP 所使用的格式。那时还算是可读性很强的,仍需要手动编辑。与 json 最明显的差别是:数组用小括号括起来并用逗号隔开元素;字典用大括号括起来并用分号隔开键值对,键值之间用等号连接;二进制数据用尖括号 括起来:

数组:

( "1", "2", "3" )

字典:

{

"key" = "value";

...

}

这也是 project.pbxproj 文件中所使用的格式。

后来出现的 GNUstep 沿用了 NeXTSTEP 格式,并添加了对 NSValue 和 NSDate 对象的支持。到了苹果的 Mac OS X 10.0 推出了新的 XML 格式,旧的 NeXTSTEP 被废弃,只支持读不支持写。这也是为什么使用 plutil 命令或者 Cocoa 的 NSPropertyListSerialization 写入 OpenStep 格式时会报错:Property list format kCFProperty ListOpenStepFormat not supported for writing

因为 XML 语法啰嗦很占空间,苹果在 Mac OS X 10.2 又推出了一种新格式,将 Property List 存储于二进制文件中。虽然在 Mac OS X 10.7 JSON 格式出现了,但是跟 Property List 不兼容。

于是乎 Property List 在苹果家族的历史上存在三种格式:OpenStep,XML 和 Binary。除了 OpenStep 被废弃不支持写入以外,其余格式都提供 API 支持读写。

操作 Property List 的途径

Unix 的 plutil 工具提供了处理 Property list 文件的能力。 比如将 Property list 文件转成 XML 格式:

plutil -convert xml1 -s -r -o project.pbxproj.xml project.pbxproj

-convert 选项可以传入的参数有: xml1, binary1 和 json。

当然 Cocoa 的 NSPropertyListSerialization 也提供了类似的功能,更面向对象。其实 plutil 和 NSPropertyListSerialization 底层都是调用 CoreFoundation 的CFPropertyList 相关的 API,所以功能类似。

使用 NSPropertyListSerialization 读入 project.pbxproj 文件时,字典中键值对的顺序会跟文件中原始的顺序不一致。这是因为字典为了实现快速查找会将 key 按序存储(比如字典序或用红黑树排序)。用 plutil 命令将 project.pbxproj 文件转成 xml 或 json 也会如此。

此外,plutil 命令也支持对某个 keypath 的增、删、改操作。NSPropertyListSerialization 就更不用说了,在程序中随意搞。

之前提到过不支持 OpenStep 写入的问题,所以即便我们能在内存中操作 project.pbxproj 文件,依然不能直接保存。如果自己动手写一个 OpenStep 格式生成程序,依然无法准确还原字典中键值对的顺序。更何况 project.pbxproj 文件中还插入了大量增强 human-readable 的注释,这些注释的生成是有特殊逻辑的,这个在后面会讲。

简要解析 project.pbxproj 文件

既然表面上无法将修改过的工程文件数据还原为 OpenStep 格式,Xcode 又是如何『开挂』做到的呢?这就得从 project.pbxproj 文件内容说起了。

内容规则

project.pbxproj 使用 UUID 作为交叉引用的索引,保证每个配置信息对象的唯一性。因为 UUID 根据机器硬件和时间戳生成,避免了多人在同一时间段操作修改工程文件带来的问题。也就是说工程中每项配置对象都有个唯一的 UUID,然后其他配置对象想引用某个配置对象直接使用它的 UUID 即可。这就跟我们编程时使用指针指向某个对象的地址一样,其他对象的属性想引用它,只需要给属性传个指针地址就行了。

可以把整个文件的内容想象成一个字典,字典中的 Key 按照字典序来排列。字典的第一层级总共有 5 个键值对,Key 分别为:archiveVersion,classes,objectVersion,objects 和 rootObject。其中重要的 Key 是 objects 和 rootObject。

所有的配置对象都放在 objects 对应的 Value 中,包括跟对象(rootObject)。 objects 对应的 Value 也是一个字典,Key 都为 UUID,Value 依然是个字典。可以将 rootObject 的值(是一个 UUID)作为 Key 在 objects 对应的字典中找到根对象。这个根对象的 isa 属性为 PBXProject(isa = PBXProject)。读懂 project.pbxproj 的最好方式就是顺着 rootObject 的各个属性对应的 UUID 在 objects 中找到对应的对象,然后一层层看下去。这样整个文件的配置信息存放方式就慢慢摸清了。

objects 中的键值对被分成了若干个 section,虽然 section 的顺序是 Xcode 私有 API 钦定的,但每个 section 内部的键值对会根据 Key 的字典序排列。

每个对象内部的属性(也是键值对)会把 isa 排在最前面,其余的按照字典序排列。

数组内部的顺序完全按照元素内容的字典序排列。

下面是 objects 中 PBXNativeTarget section 的一个对象,感受一下格式:

/* Begin PBXNativeTarget section */

A450185D1D9D68D60002869D /* projectTest */ = {

isa = PBXNativeTarget;

buildConfigurationList = A45018751D9D68D60002869D /* Build configuration list for PBXNativeTarget "projectTest" */;

buildPhases = (

A450185A1D9D68D60002869D /* Sources */,

A450185B1D9D68D60002869D /* Frameworks */,

A450185C1D9D68D60002869D /* Resources */,

);

buildRules = (

);

dependencies = (

);

name = projectTest;

productName = projectTest;

productReference = A450185E1D9D68D60002869D /* projectTest.app */;

productType = "com.apple.product-type.application";

};

/* End PBXNativeTarget section */

可以根据 A45018751D9D68D60002869D 找到对应的 buildConfigurationList 对象的内容,所以说 project.pbxproj 使用 UUID 作为交叉引用的索引。通过这种关系,可以递归构建一张有向图,每个对象都是一个节点。

内容类型

在 Xcode 中能看见所有的公共配置信息都存在于 project.pbxproj 中。主要包含跟文件相关的 BuildFile,Group 和 FileReference;跟编译相关的 BuildPhase 和 Build Configuration(List);以及一些列 Target 和 TargetDependency。

objects 的键值对根据内容类型被分成了若干个 section,采用注释的方式分节也使得可读性更强。section 的数量跟工程有关,尤其是每个工程的 BuildPhase 和 Target 差别都很大。下面列出了一个section 列表(非完整):

PBXBuildFile

PBXBuildPhase

PBXAppleScriptBuildPhase

PBXCopyFilesBuildPhase

PBXFrameworksBuildPhase

PBXHeadersBuildPhase

PBXResourcesBuildPhase

PBXShellScriptBuildPhase

PBXSourcesBuildPhase

PBXContainerItemProxy

PBXFileElement

PBXFileReference

PBXGroup

PBXVariantGroup

PBXTarget

PBXAggregateTarget

PBXLegacyTarget

PBXNativeTarget

PBXProject

PBXTargetDependency

XCBuildConfiguration

XCConfigurationList

每个 section 中的对象类型都是相同的,对象的类型是靠 isa 的值区分的。对象内部的属性类型以及含义可以参照这篇文章提供的对照表:Xcode Project File Format(http://www.monobjc.net/xcode-project-file-format.html)

操作 project.pbxproj 文件

我收集了一些可以操作 project.pbxproj 文件的优秀轮子,原理大都是用 plutil 转成 json 或 xml 后进行处理,不仅功能非常局限,且都无法完美还原为 OpenStep 格式的内容:

  • Xcodeproj CocoaPods 写的 Ruby 解析库,用于修改引入 CocoaPods 的工程文件并保存为 XML 格式。CocoaPods 本身是很强大的,还可以用来操作 Xcode workspaces (.xcworkspace), configuration files (.xcconfig) 和 Xcode Scheme files (.xcscheme).

  • mod-pbxproj 强大的 Python 解析库,支持一定的修改操作,可输出 OpenStep 格式,但是顺序和注释内容无法完美还原,有些鸡肋。

  • xUnique 用 Python 写的统一多设备生成的 UUID 的工具,主要用途是统一工程在多设备上生成的 UUID,避免工程文件冲突。

  • pbxplorer Ruby 写的解析库。

  • node-xcode Cordova 基于它管理 Xcode 工程

不过 Xcode 可以打开 XML 格式的 project.pbxproj,一旦在 Xcode 界面上修改工程配置就会重新将 project.pbxproj 转成 OpenStep 风格。解铃还须系铃人,经过多番对比之后发现最终还是 Xcode 自己才能将 XML 完美还原成原来的 OpenStep 格式,且 diff 对比毫无差错。原因很简单,Xcode 使用的私有 API 的导出结果是个黑盒,外界无论怎么猜都会有瑕疵。所以还是导出为 XML 后手动在 Xcode 界面中触发下吧。既然这样的话,如果能够简单高效地生成出 XML 文件作为工程文件就好了。基于此想法我开发了一款叫做 pbxprojHelper 的 Mac App:

操作简单粗暴:

  • 选择一个工程文件然后内容会自动解析在下面的 Outline 列表中,Filter 输入框便于过滤查看内容。

  • 单击 Outline 列表中的文字即可复制内容到剪贴板,双击复制整个keypath!

  • 对 project.pbxproj 文件的增删改操作都配置在 json 文件中,每次想对工程进行修改只需选择对应的 json 配置文件然后点击 “Apply” 即可完成写入替换哦!

  • 不小心误操作的话还可以点 “Revert” 回滚到上个版本哦!

  • 什么?懒得写 json 配置文件?下面这个附带的 json 配置生成器可以帮你直接生成一个哦!使用 ⇧⌘0 快捷键即可召唤此神器!选择两个工程文件和 json 保存路径后轻轻一点 “Generate” 就搞定咯:

所以处理工程文件的正确姿势是:

  • 拷贝出一份原始的 project.pbxproj 文件

  • 在 Xcode 界面上修改工程配置,比如修改编译选项,使用自己的证书等

  • 使用 pbxprojHelper 的 JSON Configuration Generator 来对比修改后的工程文件和原始的工程文件,自动生成 JSON 配置文件

  • 以后想要在工程文件上施加自己的修改时,只需要应用之前生成好的 JSON 配置文件即可

pbxprojHelper 的优势在于可以自由地增删改查任意属性,原生 UI 降低了使用门槛。功能强大的同时人性化的设计使得更快捷浏览工程文件中的内容。无需写任何代码即可一键配置自己想要的工程文件

此外还提供了命令行工具 pbxproj, 它具有 pbxprojHelper.app 具有的大部分功能:

Usage: pbxproj [command_option] file

Command options are (-convert is the default):

-compare modified_file -o path          compare modified property list file with property list file and generate a json result at the given path

-apply json_file                        apply a json file on property list file

-revert                                 revert property list file to latest backup

-convert                                rewrite property list files in xml format

可以使用 pbxproj 搭配 DevToolsCore 私有 framework 来完成修改工程文件并转化成 OpenStep 格式的一条龙自动化程序。

你可以在 GitHub 上下载最新的 Release 版。或者在 App Store 中下载:https://itunes.apple.com/cn/app/pbxprojhelper/id1160801848?mt=12

本项目完全手撸,没依赖上面提到的任何轮子

聊聊 Xcode 项目文件中的 project.pbxproj的更多相关文章

  1. XCode工程中 Project 和 Targets区别

    转自:http://blog.csdn.net/zhaozy55555/article/details/8557175 project就是一个项目,或者说工程,一个project可以对应多个targe ...

  2. #iPhone6与iPhone6Plus适配#如何在Xcode 6中创建 PCH 文件

    本文永久链接http://www.cnblogs.com/ChenYilong/p/4008086.html   新建文件 ⌘+N选择 iOS/Mac -> Other -> PCH Fi ...

  3. 大面积project.pbxproj冲突问题解决

    在团队开发中,经常会有project.pbxproj的冲突出现. 所以我们添加过新的文件后,要及时的提交,养成好习惯.以免出问题. 但是总有一些时候忘记提交出现大面积的冲突,然后把==== <& ...

  4. [译]聊聊C#中的泛型的使用(新手勿入) Seaching TreeVIew WPF 可编辑树Ztree的使用(包括对后台数据库的增删改查) 字段和属性的区别 C# 遍历Dictionary并修改其中的Value 学习笔记——异步 程序员常说的「哈希表」是个什么鬼?

    [译]聊聊C#中的泛型的使用(新手勿入)   写在前面 今天忙里偷闲在浏览外文的时候看到一篇讲C#中泛型的使用的文章,因此加上本人的理解以及四级没过的英语水平斗胆给大伙进行了翻译,当然在翻译的过程中发 ...

  5. 项目文件中的已知 NuGet 属性(使用这些属性,创建 NuGet 包就可以不需要 nuspec 文件啦)

    知道了 csproj 文件中的一些常用 NuGet 属性,创建 NuGet 包时就可以充分发挥新 Sdk 自动生成 NuGet 包的优势,不需要 nuspec 文件啦.(毕竟 nuspec 文件没有 ...

  6. 使用Xcode改动iOS项目project名和路径名

    对,好.错.改正. ------ 前言 系统 10.9 开发平台 xcode 5.0 旧project名 MyProject-iPad 改动之后 新project名 FjSk-iPad 点击项目,进入 ...

  7. project.pbxproj 的merge问题

    基于xcode8.0 1.project.pbxproj 的结构 内部文件{archiveVersion=1 ; classes={};objectVersion=46;objects={};root ...

  8. Xcode开发中 Code Snippets Library 的相关用法

    当在进行项目的时候,总会遇到很多相同的写法.因此,我们可以使用Code Snippets Library 来进行代码小片段的“封装”: 以Xcode中常用的属性为例: 使用步骤如下: 1.在Xcode ...

  9. XCODE UITextField 中的属性和用法

    XCODE  UITextField  中的属性和用法 一些基本的用法 UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedR ...

随机推荐

  1. Hadoop MapReduce概念学习系列之shuffle大揭秘(十九)

    shuffle是非常重要!一定要深入理解和多实践. 缓存,分组,排序,转发,这些都是mr的shuffle. Soga 我想得到按流量来排序,而且还是倒序,怎么达到实现呢?这就牵扯到排序的的问题 默认是 ...

  2. Ubuntu下gdb远程调试--warning: Could not load vsyscall page because no executable was specified解决方案

    1. 首先安装gdbserver apt-get install gdbserver 2. 编译-g 程序 gcc -g test_gdb.c -o test_gdb 源码如下: #include & ...

  3. 1.VS2010C++环境设置

    一.需要下载的软件 1.visual studio 2010\\xxzx\tools\编程工具\MICROSOFT\VISUAL.STUDIO\VISUAL.STUDIO.201032位cn_visu ...

  4. VISA资源名称控件

    NI-VISA能自动检测端口.通过前面板上的VISA资源名称控件或VISA查找资源函数可查看端口列表.在任何平台上,NI-VISA支持的最大串口数量为256,串口的默认数量取决于操作系统. VISA资 ...

  5. OSPF虚链路配置.示例1

      在OSPF 网络中,区域0为骨干区域,其它的为非骨干区域,非骨干区域必须与骨干区域直接相连. 根据拓扑图可看到区域1与骨干区域0直接相连而区域2与骨干区域没有直接相连,这种情况下我们可以创建一条虚 ...

  6. css3 动画demo

    1)http://www.yyyweb.com/demo/css-cokecan/inner.html 2)页面切换效果demo http://www.yyyweb.com/demo/page-tra ...

  7. C++视频课程小结(1)

    C++远征之起航篇 章节介绍: 每章小结: 第一章:C++诞生于贝尔实验室,C++包含C语言. 第二章:介绍了IDE环境(虽然没怎么懂),还推荐使用visual stdio 2010 旗舰版(姑且下了 ...

  8. iOS的几种定时器

    //gcd的定时器timer必须先保存为一个属性或者成员变量 @property (nonatomic , assign) dispatch_source_t timer; //第一种 每一秒执行一次 ...

  9. vtk读取文件中点坐标[转]

    vtk基础编程(2)-读取数据文件中的坐标点 1. 案例说明 在实际计算中,常常需要大量的数据, 这个时候数据文件就必不可少, 例如 数据文件points.dat, 中存放了三个点的坐标, 0.0 0 ...

  10. MFC中 Invalidate() , InvalidateRect() , UpdateWindow(), Redrawwindow() 区别

    1. void Invalidate( BOOL bErase = TRUE ); 该函数的作用是使整个窗口客户区无效.窗口的客户区无效意味着需要重绘,例如,如果一个被其它窗口遮住的窗口变成了前台窗口 ...