[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的结构图 (原创):

[iOS]XcodeProject的内部结构分析_第1张图片
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框架的使用( 二 )

你可能感兴趣的:([iOS]XcodeProject的内部结构分析)