聊聊 Xcode 项目文件中的 project.pbxproj
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的更多相关文章
- XCode工程中 Project 和 Targets区别
转自:http://blog.csdn.net/zhaozy55555/article/details/8557175 project就是一个项目,或者说工程,一个project可以对应多个targe ...
- #iPhone6与iPhone6Plus适配#如何在Xcode 6中创建 PCH 文件
本文永久链接http://www.cnblogs.com/ChenYilong/p/4008086.html 新建文件 ⌘+N选择 iOS/Mac -> Other -> PCH Fi ...
- 大面积project.pbxproj冲突问题解决
在团队开发中,经常会有project.pbxproj的冲突出现. 所以我们添加过新的文件后,要及时的提交,养成好习惯.以免出问题. 但是总有一些时候忘记提交出现大面积的冲突,然后把==== <& ...
- [译]聊聊C#中的泛型的使用(新手勿入) Seaching TreeVIew WPF 可编辑树Ztree的使用(包括对后台数据库的增删改查) 字段和属性的区别 C# 遍历Dictionary并修改其中的Value 学习笔记——异步 程序员常说的「哈希表」是个什么鬼?
[译]聊聊C#中的泛型的使用(新手勿入) 写在前面 今天忙里偷闲在浏览外文的时候看到一篇讲C#中泛型的使用的文章,因此加上本人的理解以及四级没过的英语水平斗胆给大伙进行了翻译,当然在翻译的过程中发 ...
- 项目文件中的已知 NuGet 属性(使用这些属性,创建 NuGet 包就可以不需要 nuspec 文件啦)
知道了 csproj 文件中的一些常用 NuGet 属性,创建 NuGet 包时就可以充分发挥新 Sdk 自动生成 NuGet 包的优势,不需要 nuspec 文件啦.(毕竟 nuspec 文件没有 ...
- 使用Xcode改动iOS项目project名和路径名
对,好.错.改正. ------ 前言 系统 10.9 开发平台 xcode 5.0 旧project名 MyProject-iPad 改动之后 新project名 FjSk-iPad 点击项目,进入 ...
- project.pbxproj 的merge问题
基于xcode8.0 1.project.pbxproj 的结构 内部文件{archiveVersion=1 ; classes={};objectVersion=46;objects={};root ...
- Xcode开发中 Code Snippets Library 的相关用法
当在进行项目的时候,总会遇到很多相同的写法.因此,我们可以使用Code Snippets Library 来进行代码小片段的“封装”: 以Xcode中常用的属性为例: 使用步骤如下: 1.在Xcode ...
- XCODE UITextField 中的属性和用法
XCODE UITextField 中的属性和用法 一些基本的用法 UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedR ...
随机推荐
- Linux下的grep搜索命令详解(二)
grep与正规表达式 字符类 字符类的搜索:如果我想要搜寻 test 或 tast 这两个单词时,可以发现到,其实她们有共通的 't?st' 存在-这个时候,我可以这样来搜寻: [root@www ...
- C++11用于元编程的类别属性
[C++11用于元编程的类别属性] 许多算法能作用在不同的数据类别; C++ 模板支持泛型,这使得代码能更紧凑和有用.然而,算法经常会需要目前作用的数据类别的信息.这种信息可以通过类别属性 (type ...
- LightOJ 1259 Goldbach`s Conjecture (哥德巴赫猜想 + 素数筛选法)
http://lightoj.com/volume_showproblem.php?problem=1259 题目大意:给你一个数n,这个数能分成两个素数a.b,n = a + b且a<=b,问 ...
- AutoCAD.NET 不使用P/Invoke方式调用acad.exe或accore.dll中的接口(如acedCommand、acedPostCommand等)
使用C#进行AutoCAD二次开发,有时候由于C#接口不够完善,或者低版本AutoCAD中的接口缺少,有些工作不能直接通过C#接口来实现,所以需要通过P/Invoke的方式调用AutoCAD的其他DL ...
- JQuery点击收起,点击展开以及部分非空小验证
<tr> <td nowrap align="right" width="18%"> 解决方案: </td> <td ...
- How Tomcat Works(十一)
本文接下来分析tomcat的类载入器,tomcat需要实现一个自定义的载入器,而不能使用系统类载入器 (1)限制serlvet访问当前运行的java虚拟机中环境变量CLASSPATH指明的路径下的所有 ...
- html 4.01速查手册
来自 W3School 的 HTML 快速参考.可以打印它,以备日常使用. HTML Basic Document <html> <head> <title>Doc ...
- bmp格式解析
最近一直在写图像处理的作业,好多啊 bmp格式简介 a.格式组成 1:位图头文件数据结构,它包含BMP图像文件的类型.显示内容等信息: 2:位图信息数据结构,它包含有BMP图像的宽.高.压缩方法,以及 ...
- OC中控制台日志打印
OC中Debug版本常用的打印格式化操作 %@ 对象 %d,%i 整型 (%i的老写法) %hd 短整型 %ld , %lld 长整型 %u 无符整型 %f 浮点型和doubl ...
- 查找DOM
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8&quo ...