Xcode中project.pbxproj 合并冲突的终极解决方法

引言

Xcode的工程文件是 工程名.xcodeproj,它其实是个package包,通过显示包内容,可以查看到它内部主要有project.pbxproj 和 xcuserdata,以及xcshareddata。其中,xcuserdata 一般是跟用户相关的一些设置,如断点 记录等,一般不用放到版本管理中。而project.pbxproj 是工程描述文件,描述了工程里的源码文件、scheme设置等。它的格式是文本类型的plist(Info.plist是binary plist),里面是一个一个的object。
当团队中多人同时开发或者进行项目架构调整时,首先会出现冲突的地方就是这里。尤其是已经经历很长开发周期的老项目,升级改造时,随着文件的新建、删除、以及各种移动等等。各分支merge时带来的工程文件冲突十分令人头疼。对于这种project.pbxproj冲突,目前没有什么好的解决办法,只能人工逐个识别判断,稍有不慎。可能xcode就打不开了。
那么,怎么办呢???笔者最近参与一大型项目的重构,因为项目启动开发时间较早,在开发周期长,文件数量大。分支merge时,遇到的project.pbxproj冲突,十分头疼。最正在一次解决冲突的过程中,受到一位同事启发。用此方法来解决project.pbxproj冲突,简直事半功倍。那到底是什么方法 ---- 3-6法则。在讲3-6法则前先普及下基本知识:对project.pbxproj作简要说明。

pbxproj文件简要说明

pbxproj是个plist文件,plist的格式跟json的差不多,就是一个个对象,对象是个字典,可以关联一些字段和它的值。pbxproj的总体框架如下:

// !$*UTF8*$!
{
    archiveVersion = 1;
    classes = {
    };
    objectVersion = 45;
    objects = {
            /* ... */
    };
    rootObject = 29B97313FDCFA39411CA2CEA /* Project object */;
}

其中objects是主要的字段。它本身是一个大哈希,里面包含了一个个的键值对。如下:

//1、PBXFileReference
1A36EFE51CEAC506005A5035 /* DiscoverManager.h */ = {
isa = PBXFileReference;
fileEncoding = 4; 
lastKnownFileType = sourcecode.c.h; 
path = DiscoverManager.h; 
sourceTree = "<group>"; 
};

//2、PBXBuildFile
1A1282EE1C069969000C36AA /* ScreenCaptureViewController.m in Sources */ = {
isa = PBXBuildFile;
 fileRef = 1A1282ED1C069969000C36AA /* ScreenCaptureViewController.m */; 
};

//3、PBXSourcesBuildPhase
BF3014D41C10632C0080D38E /* Sources */ = {
    isa = PBXSourcesBuildPhase;
    buildActionMask = 2147483647;
    files = (
        BF3015161C10700E0080D38E /* AAStable3ViewController.m in Sources */,
        BF3015101C106FD70080D38E /* AAStable1ViewController.m in Sources */,
        BF3015221C10707E0080D38E /* AAFileMayMoveViewController.m in Sources */,
    );
    runOnlyForDeploymentPostprocessing = 0;
};

//4、PBXResourcesBuildPhase
BF3014D61C10632C0080D38E /* Resources */ = {
    isa = PBXResourcesBuildPhase;
    buildActionMask = 2147483647;
    files = (
        BF3014EB1C10632C0080D38E /* LaunchScreen.storyboard in Resources */,
        BF3014E81C10632C0080D38E /* Assets.xcassets in Resources */,
        BF3014E61C10632C0080D38E /* Main.storyboard in Resources */,
        BF3014E61C10632C0080D38E /* coverstory_done_highlight@3x.png in Resources */,
    );
    runOnlyForDeploymentPostprocessing = 0;
};

//5、PBXGroup
BF3014CF1C10632C0080D38E = {
    isa = PBXGroup;
    children = (
        BF3014DA1C10632C0080D38E /* PBTest */,
        BF3014F41C10632C0080D38E /* PBTestTests */,
        BF3014FF1C10632D0080D38E /* PBTestUITests */,
        BF3014D91C10632C0080D38E /* Products */,
    );
    sourceTree = "<group>";
};

这里的BF3014CF1C10632C0080D38E模样的数据 是uuid,后面又是一个对象。(1)每个对象中的对象都有一个isa字段,用来表明了object的类型。(上面我们共列举了5中类型,还有其他类型,这里我们只重点介绍这五种)
(2)对象中的其他字段取决于object的类型。

objects中根据uuid和对象的关联,就可以唯一标识这个对象,方便对象的相互引用。例如:通过uuid,PBXFileReference 类型的对象可以被PBXBuildFile和PBXGroup对象引用,PBXBuildFile 对象可以被PBXSourcesBuildPhase 对象引用。

针对常用的类型做简要说明:

1、PBXFileReference 用来跟踪工程中使用的外部文件(对应到磁盘),包括源文件、头文件、资源文件、库、生成的应用文件等。(简单理解就是,工程中引用到的所有类型的文件,.h.m\storyboard\Pods-News.debugadhoc.xcconfig 等等)它会被PBXGroup、PBXBuildFile等调用。

PXBFileReference类型的objc的结构大体如下:(在看其结构时,重点关注.m和.h文件的数目,这关系到我们上面提到的3-6原则)

/* Begin PBXFileReference section */
//示例1
1A58A5351CE03FA70020DE69 /* MomoChatShareService.m */ = { (1次)
isa = PBXFileReference; 
fileEncoding = 4; 
lastKnownFileType = sourcecode.c.objc; 
path = MomoChatShareService.m; (1次)
sourceTree = "<group>"; 
};
//示例2
1A58A5391CE0407C0020DE69 /* MomoChatSDK.h */ = { (1次)
isa = PBXFileReference;
 fileEncoding = 4; 
lastKnownFileType = sourcecode.c.h; 
path = MomoChatSDK.h; (1次)
sourceTree = "<group>"; 
};
//示例3
1A58A5391CE0407C0020DE69 /* Base */ = {
    isa = PBXFileReference; 
    lastKnownFileType = file.storyboard; 
    name = Base;
    path = Base.lproj/Main.storyboard; 
    sourceTree = "<group>"; 
};
......
/* End PBXFileReference section */

以MomoChatShareService.m这个.m文件为例,它在PBXFileReference section中出现了2次。同样,MomoChatSDK.h这个.h文件也出现了2次。我们暂且先记住他的次数。

2、PBXBuildFile :参与编译的PBXFileReference会有对应的PBXBuildFile,它会被PBXSourcesBuildPhase或PBXResourcesBuildPhase调用,这里一般不会有.h文件。PBXBuildFile类型的objc的结构大体如下:

/* Begin PBXBuildFile section */

//示例1
4B17C2FB283B5E0EDF457674 /* libPods-WLRRoute_Example.a in Frameworks */ = {
isa = PBXBuildFile;
 fileRef = 345940877F636B03192F1CA8 /* libPods-WLRRoute_Example.a */;
 };

//示例2
6003F58E195388D20070C39A /* Foundation.framework in Frameworks */ = {
isa = PBXBuildFile; 
fileRef = 6003F58D195388D20070C39A /* Foundation.framework */; 
};

//示例3
1A58A5351CE03FA70020DE69 /* MomoChatShareService.m in Sources */ = {(1次)
isa = PBXBuildFile; 
fileRef = 76B4BC811E06A18400D1E590 /* MomoChatShareService.m */; (1次)
};
/* End PBXBuildFile section */

这个对象总包含了两个key值,isa和fileRef,分别用来指明对象的类型和它调用的PBXFileReference。
我们再来看下MomoChatShareService.m出现的次数,共2次,分别在name和fileRef中各包含一次。这时注意观察PBXBuildFile中包含MomoChatShareService.m的objc的UUID,同PBXFileReference中包含MomoChatShareService.m的objc的UUID是相同的。而且一定是相同的,只有相同的UUID才能唯一标识一个文件。

3、PBXSourcesBuildPhase:列出工程中参与编译的文件(Xcode中Build Phases下的Compiles Source)。如果有多个target,则会有多个source,如UITest、UNIT-Test都会生成source,下面是主target的source :

/* Begin PBXSourcesBuildPhase section */
//示例1:主工程target
6003F586195388D20070C39A /* Sources */ = {
            isa = PBXSourcesBuildPhase;
            buildActionMask = 2147483647;
            files = (
                1A58A5351CE03FA70020DE69 /* MomoChatShareService.m in Sources */ (1次)
                6003F5A7195388D20070C39A /* ViewController.m in Sources */,
                76B4BC851E06A18E00D1E590 /* UserHandler.m in Sources */,
                76B4BC8B1E06A1AA00D1E590 /* UserViewController.m in Sources */,
                6003F59A195388D20070C39A /* main.m in Sources */,
                76B4BC881E06A1A100D1E590 /* SignViewController.m in Sources */,
                                .......
            );
            runOnlyForDeploymentPostprocessing = 0;
        };
//示例2:test target
6003F5AA195388D20070C39A /* Sources */ = {
            isa = PBXSourcesBuildPhase;
            buildActionMask = 2147483647;
            files = (
                6003F5BC195388D20070C39A /* Tests.m in Sources */,
            );
            runOnlyForDeploymentPostprocessing = 0;
        };
/* End PBXSourcesBuildPhase section */

PBXSourcesBuildPhase中主要有两个重要的key:isa 和 files。分别表明对象的类型和他所包含参与编译的文件。
这时我们看到MomoChatShareService.m文件的出现的次数为1,到目前为止MomoChatShareService.m在整个工程文件中出现的次数为5次。(这里边不会出现.h文件。)

4、PBXResourcesBuildPhase:包含了工程中编译的资源文件(如图片、storyBoard等),PBXResourcesBuildPhase的结构如下:

/* Begin PBXResourcesBuildPhase section */
        6003F588195388D20070C39A /* Resources */ = {
            isa = PBXResourcesBuildPhase;
            buildActionMask = 2147483647;
            files = (
                873B8AEB1B1F5CCA007FD442 /* Main.storyboard in Resources */,
                6003F5A9195388D20070C39A /* Images.xcassets in Resources */,
                6003F598195388D20070C39A /* InfoPlist.strings in Resources */,
                                06ED2FDA1B29656D007679A4 /* kr-video-player-pause@3x.png in Resources */,
            );
            runOnlyForDeploymentPostprocessing = 0;
        };
        6003F5AC195388D20070C39A /* Resources */ = {
            isa = PBXResourcesBuildPhase;
            buildActionMask = 2147483647;
            files = (
                6003F5BA195388D20070C39A /* InfoPlist.strings in Resources */,
            );
            runOnlyForDeploymentPostprocessing = 0;
        };
/* End PBXResourcesBuildPhase section */

PBXResourcesBuildPhase 对象中同样有两个重要的key,isa 和 files ,分别表明对象的类型和工程编译用到的资源文件,例如图片、storyboard等。
显然,这里面不会包含.h和.m文件。

5、PBXGroup 对应工程中的group。也就是我们开发时划分的目录,

例如:工程中的目录结构如下图所示:

2017-05-13 23.17.16.png

对应的PBXGroup结构如下:

/* Begin PBXGroup section */
        060D3DF51EC75917001A30BE /* Classes */ = {
            isa = PBXGroup;
            children = (
                060D3DF61EC75917001A30BE /* VideoPalyerView */,
                060D3DF91EC75917001A30BE /* VideoPlayerVC */,
                060D3DFE1EC75937001A30BE /* VideoPlayerModel.h */,
                060D3DFF1EC75937001A30BE /* VideoPlayerModel.m */,
            );
            path = Classes;
            sourceTree = "<group>";
        };
                //子目录中的内容,使用单独的对象来展示
        060D3DF61EC75917001A30BE /* VideoPalyerView */ = {
            isa = PBXGroup;
            children = (
                060D3DF71EC75917001A30BE /* KRVideoPlayerControlView.h */,
                060D3DF81EC75917001A30BE /* KRVideoPlayerControlView.m */,
            );
            path = VideoPalyerView;
            sourceTree = "<group>";
        };
                //子目录中的内容,使用单独的对象来展示
        060D3DF91EC75917001A30BE /* VideoPlayerVC */ = {
            isa = PBXGroup;
            children = (
                060D3DFA1EC75917001A30BE /* KRVideoPlayerController.h */,
                060D3DFB1EC75917001A30BE /* KRVideoPlayerController.m */,
            );
            path = VideoPlayerVC;
            sourceTree = "<group>";
        };
        1A58A5351CE03FA70020DE69 /* ChatModule */ = {
            isa = PBXGroup;
            children = (
                 1A58A5351CE03FA70020DE69 /* MomoChatShareService.h */,
                            1A58A5351CE03FA70020DE69 /* MomoChatShareService.m */,(1次)
            );
            name = ChatModule;
            sourceTree = "<group>";
        };
              1A58A5391CE0407C0020DE69 /* MomoChatSDK */ = {
            isa = PBXGroup;
            children = (
                1A58A5391CE0407C0020DE69 /* MomoChatSDK.h */,(1次)
                            1A58A5391CE0407C0020DE69 /* MomoChatSDK.m */,
            );
            name = ChatModule;
            sourceTree = "<group>";
        };
    ......

/* End PBXGroup section */

PBXGroup同工程中的group是一致的,如果工程中的某个目录下包含子目录,则在其children字段中只会显示相应的子目录名称,子目录下的内容会单独创建一个object对象来展示。例如:Classes目录下,包含了VideoPalyerView和VideoPlayerVC两个子目录,在Classes objc的children中只会包含子目录的名称。对于VideoPalyerView和VideoPlayerVC这两个子目录中的内容,使用单独的PBXGroup对象来标识。
MomoChatShareService.m 在PBXGroup中共出现1次,这时我们查看对应的objc的UUID同PBXSourceFile、PBXFileReference、PBXBuildFile中包含MomoChatShareService.m的objc的UUID完全相同。也一定会相同。至此MomoChatShareService.m 在工程文件中共出现6次。包含该文件的objc每一处的UUID都是相等的。
** MomoChatSDK.h在PBXGroup中共出现1次,这时我们查看对应的objc的UUID同PBXFileReference中包含 MomoChatSDK.h的objc的UUID完全相同。至此, MomoChatSDK.h** 在工程文件中共出现3次。包含该文件的objc每一处的UUID都是相等的。

结论:

所谓的3-6法则就是:在工程文件中,某个类的.m文件一定只有6处,.h文件只有3处。该法则适用于所有的.m和.h文件。

应用

有了3-6法则后,我们怎么使用该法则呢?很简单,当出现冲突时,分别全局查找冲突文件的.m和.h文件的总数。只要是少于6或者3个的文件一般是新的工程中不存在的文件,可直接将该文件删掉。而多余6或者3个文件的则需要将多余的文件删掉。那怎么才能确定那些事多余的文件呢??这就需要用到UUID,通过比对冲突处包含.m或者.h文件的objc的UUID是否跟其他位置上对应文件的UUID相同,相同则保留,反之则删除。

自动化脚本

正在完善中,请期待...

推荐阅读更多精彩内容