[iOS]XcodeProject的内部结构分析

1. 背景

平时开发中,经常会遇到xcodeproj冲突,就需要打开这个文件,进行处理。当然现在也有很多工具或者自动化的脚本来自动merge,比如 simonwagner/mergepbx, 但这个文件错综复杂,尤其当项目大到一定阶段后,非常可怕,所以很多情况还是需要人工来处理各种冲突。

所以决定研究下,这个庞然大物内部的结构究竟是怎样的...

2. XcodeProj的工程结构

2.1 project.pbxproj文件

Xcode每个项目的工程文件都在xxx.xcodeproj中,查看包内容,可以看到真正的内容都在project.pbxproj里面,其他有一些xcuserdata之类的,不重要,先忽略。我们主要来看pbxproj文件。

这是一个plist文件。记录了所有代码的和库文件的索引和路径信息,以及Target信息,包括Build Setting/Build Phase等等信息。

虽然xcodeproj提供了很多方便的文件管理和索引,但我自身还是更喜欢无project文件的代码,直接与物理目录对应,不要与IDE产生太多的依赖和耦合。

2.2 pbxproj预览

一个完整的pbxproj文件基本是如下这样:

// !$*UTF8*$!
{
    archiveVersion = 1;
    classes = {
    };
    objectVersion = 46;
    objects = {
        …
    };
    rootObject = 4B74E19C1AB185A200A5A377 /* Project object */;
}

核心内容都在objects中,完整版如下:

// !$*UTF8*$!
{
    archiveVersion = 1;
    classes = {
    };
    objectVersion = 46;
    objects = {
        /* 构建所需的代码文件,资源文件,库文件等 */
        /* 平时git发生冲突也主要是在这个区域内冲突 */
        /* 你每新建一个.h/.m文件,就会修改这个区域, 各个branch都在创建的时候,容易冲突 */
        /* Begin PBXBuildFile section */
            ...
        /* End PBXBuildFile section */
        
        /* 这里记录了每个target的targetProxy,与PBXTargetDependency相对应 */
        /* Begin PBXContainerItemProxy  section */
            ...
        /* End PBXContainerItemProxy section */
        
        /* 主要记录每个target的BuildPhase中的Embed App Extensions的部分 */
        /* Begin PBXCopyFilesBuildPhase section */
        /* End PBXCopyFilesBuildPhase section */

        /* 记录了每个代码文件的文件类型、路径path、sourceTree,不论引入文件的时候是create group还是create reference,都会在这里添加一条记录 */
        /* Begin PBXFileReference section */
        /* End PBXFileReference section */

        /* 工程中所依赖的Frameworks的信息,对应Build Phases中的`Link Binary With Libraries` */
        /* Begin PBXFrameworksBuildPhase section */
        /* End PBXFrameworksBuildPhase section */

        /* 工程中所有文件的group信息,这个和xcode文件目录是对应的,每一层的文件目录有唯一的UUID,同一层group下的子group会和上一层的group的UUID有很高的重合度(基本只有1-2位不同),这个PBXGroup section中,子group没有用树的方式,而是采用类似列表的方式呈现了所有的group目录,可以脑补:打开xcode左侧目录,然后让所有目录和文件"左对齐",然后就会生成如下的结构` */
        /* Begin PBXGroup section */ 
        /* End PBXGroup section */

        /* 每个Target的BuildSettings和BuildPhases(Sources/Frameworks/Resources等)的信息 */
        /* Begin PBXNativeTarget section */  
        /* End PBXNativeTarget section */

        /* 整个项目工程Project的信息,包括项目路径、Config信息,相关版本号,所有的Target等信息 */
        /* Begin PBXProject section */
        4B74E19C1AB185A200A5A377 /* Project object */ = {
            isa = PBXProject;
            attributes = { ... };
            ...
            targets = (
                4B74E1A31AB185A200A5A377 /* xxxxPolenTestxxxx */,
                ...
            );
        };
        /* End PBXProject section */

        /* 列举了项目中每个Resources的信息, 包括Build Phase下`Copy Bundle Resources`文件、Assets.xcassets等资源文件 */
        /* Begin PBXResourcesBuildPhase section */
        /* End PBXResourcesBuildPhase section */

        /* 对应Xcode中Build Phases下的脚本文件,包括:Embed Pods Frameworks,Check Pods Manifest.lock以及其他本地或者第三方的脚本文件信息 */
        /* Begin PBXShellScriptBuildPhase section */
        /* End PBXShellScriptBuildPhase section */

        /* 对应Xcode中Build Phases的Complie Sources的代码文件 */
        /* Begin PBXSourcesBuildPhase section */
        /* End PBXSourcesBuildPhase section */

        /* 记录了每个Target的targetProxy,每个targetProxy都是一个PBXContainerItemProxy类型,暂时没找到Xcode中的对应项 */
        /* Begin PBXTargetDependency section */
        /* End PBXTargetDependency section */

        /* 不同地区的资源文件的引用信息,如果你项目使用了国际化,相关的xxx.string就在这个section中 */
        /* Begin PBXVariantGroup section */
        /* End PBXVariantGroup section */

        /* 对应Xcode中 Build Settings中的配置信息 */
        /* Begin XCBuildConfiguration section */
        /* End XCBuildConfiguration section */

        /* XCBuildConfiguration只是列举了所有Target的所有Setting项,下面这个文件区分,不同Target在Debug时使用哪个Setting项,在Release时使用哪个Setting项 */
        /* Begin XCConfigurationList section */
        /* End XCConfigurationList section */
    };
    rootObject = 4B74E19C1AB185A200A5A377 /* Project object */;
}

2.3 pbxproj结构

2.3.1 结构图

首先可以去看一下Xcode Project File Format , 很详细的介绍了pbxproj中每个类和及其属性字段的含义和引用关系。

从Plist的角度看,我们可以将PBXProject看成一个个节点和子节点的树形结构,但从面向对象的角度,其实就是一个个类和子类。

自己简单整理了一下pbxproj的结构图 (原创):

xcodeproject_2018-08-06_23.png

2.3.2 Class Hierarchy

下面具体说一下每个节点/类 模块包含的内容以及在Xcode中对应哪些文件或者目录:

  • PBXBuildFile: 构建所需的代码文件,资源文件,库文件等
  • PBXBuildPhase: 对应Xcode中Build Phases
    • PBXAppleScriptBuildPhase
    • PBXCopyFilesBuildPhase: 主要记录每个target的BuildPhase中的Embed App Extensions的部分
    • PBXFrameworksBuildPhase: 工程中所依赖的Frameworks的信息,对应Build Phases中的Link Binary With Libraries
    • PBXHeadersBuildPhase
    • PBXResourcesBuildPhase: 列举了项目中每个Resources的信息, 包括Build Phase下Copy Bundle Resources文件、Assets.xcassets等资源文件
    • PBXShellScriptBuildPhase : 对应Xcode中Build Phases下的脚本文件,包括:Embed Pods Frameworks,Check Pods Manifest.lock以及其他本地或者第三方的脚本文件信息
    • PBXSourcesBuildPhase: 对应Xcode中Build Phases的Complie Sources的代码文件
  • PBXContainerItemProxy: 这里记录了每个target的targetProxy,与PBXTargetDependency相对应
  • PBXFileElement
    • PBXFileReference: 记录了每个代码文件的文件类型、路径path、sourceTree, 不论引入文件的时候是create group还是create reference,都会在这里添加一条记录
    • PBXGroup:工程中所有文件的group信息,这个和xcode文件目录是对应的,每一层的文件目录有唯一的UUID,同一层group下的子group会和上一层的group的UUID有很高的重合度(基本只有1-2位不同),这个PBXGroup section中,子group没有用树的方式,而是采用类似列表的方式呈现了所有的group目录,可以脑补:打开xcode左侧目录,然后让所有目录和文件"左对齐",然后就会生成如下的结构`
    • PBXVariantGroup: 不同地区的资源文件的引用信息,如果你项目使用了国际化,相关的xxx.string就在这个section中
  • PBXTarget: 每个Target的BuildSettings和BuildPhases(Sources/Frameworks/Resources等)的信息
    • PBXAggregateTarget: TODO: 暂未找到相关介绍,自己的项目里也没出现这类Target
    • PBXLegacyTarget:TODO: 暂未找到相关介绍,自己的项目里也没出现这类Target
    • PBXNativeTarget: 正常建立的Target都是这种类型的
  • PBXProject:整个项目工程Project的信息,包括项目路径、Config信息,相关版本号,所有的Target等信息
  • PBXTargetDependency: 记录了每个Target的targetProxy,每个targetProxy都是一个PBXContainerItemProxy类型,暂时没找到Xcode中的对应项
  • XCBuildConfiguration: 对应Xcode中 Build Settings中的配置信息
  • XCConfigurationList: XCBuildConfiguration只是列举了所有Target的所有Setting项,下面这个文件区分,不同Target在Debug时使用哪个Setting项,在Release时使用哪个Setting项

分享一点其他人的总结:

  • PBXProject 为根节点,代表着整个工程

  • PBXProject 可以有多个PBXNativeTarget,代表着工程中的target

  • PBXNativeTarget 维护着各自资源文件(PBXResourcesBuildPhase),源文件(PBXSourcesBuildPhase),以及依赖库(PBXFrameworksBuildPhase)等等

  • PBXProject 和 PBXNativeTarget 都有配置管理,通过XCConfigurationList和XCBuildConfiguration维护

  • 每个导入工程的文件都会有相应的PBXFileReference记录,如果该文件在导入时,选择了create groups ,会在相应的PBXGroup中有记录

  • 每个在编译打包过程中被包含到可执行文件中的文件,都会有PBXBuildFile记录,根据类别分别在PBXResourcesBuildPhase,PBXSourcesBuildPhase等中有记录

    --- From ehyubewb, 2018

2.3.4 Reference Hierarchy

XcodeProj本身所有的引用是基于每个对象的UUID的, pbxplorer 这个库实现了对xcodeproj的解析,他在实现过程中,Reference Hierarchy如下:

PBXProject
    build_configuration_list: XCConfigurationList
    main_group: PBXGroup
    targets: [PBXNativeTarget]
  
XCConfigurationList
    build_configurations: [XCBuildConfiguration]
  
PBXGroup
    children: [PBXGroup|PBXFileReference]
    subgroups: [PBXGroup]
    file_refs: [PBXFileReference]
    variant_groups: [PBXVariantGroup]

PBXNativeTarget
    build_configuration_list: XCBuildConfigurationList
    build_phases: [PBXBuildPhase]
    product_file_ref: PBXFileReference

PBXBuildPhase
    build_files: [PBXBuildFile]

PBXBuildFile
    file_ref: PBXFileReference

如果想自己开发一套XcodeProj的框架或者处理脚本,可以参照这个Reference Hierarchy,对于类之间的彼此关联会更加清楚。

3. 总结

XcodeProj大体来说就是配置了项目的文件路径信息PBXBuildFile、项目中的Target及其依赖信息、编译中的Config信息(PBXBuildPhase、XCBuildConfiguration等)。大致了解了他的结构后,就会觉得虽然各方面井然有序,基于UUID实现关联,但整体还是显得过于庞大。尤其当项目越来越大的时候,XcodeProj打开就是一场噩梦。

对于我个人而言,我更喜欢简单轻量级的IDE模式,类似Sublime/VSCode。假设作以下改变:

对于其中的文件信息,如果Xcode不考虑支持各种Group模式,完全物理实体目录一一对应的话,那就只剩下一些Target和依赖库信息和相关的Config信息了。那这些信息本质上就是一些Config信息。那这些Config再按照Build、Info、Res等分类为不同的Config,每个Config用json实现具体的内容。

那么这样一簇Config信息+源代码文件组成的一个Project,就可以不束缚于唯一的IDE了,可以在一些常用的IDE中快速实现开发功能。

当然要考虑Debug,得再加上Clang编译器的能力,加上快捷提示的功能,加上...

那进一步想一想,如果我们自己写一个IDE, 我们需要做哪些准备呢?


参考资料

  1. GitHub: CocoaPods 官方源码
  2. GitHub: mjmsmith/pbxplorer
  3. Xcode Project File Format: 对.pbxproj文件每个参数的详细介绍
  4. XCode工程文件结构及Xcodeproj框架的使用( 二 )
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容

  • 白思璇阅读 115评论 0 0
  • 在镜前的自己,因为自己弱小,才有理埋怨朋友;因为自己弱小,才勇敢说出伤害朋友的话;因为自己弱小,周围一切都成为了敌...
    小丿年阅读 176评论 0 0
  • 有人说这是脑洞题,也就是应用题,也有人分析这是侦探题,是医学题。 姜思达有个点是很好的,记忆的重量是人生必要的承重...
    六月花阅读 432评论 0 0