美文网首页iOS DeviOS知识收集iOS零碎知识
Xcode中project.pbxproj合并冲突的解决

Xcode中project.pbxproj合并冲突的解决

作者: 俞子将 | 来源:发表于2015-12-04 10:56 被阅读9434次

    引言

    Xcode的工程文件是 工程名.xcodeproj,而它其实是个package目录,通过显示包内容,可以查看到它内部主要有project.pbxprojxcuserdata。其中,xcuserdata 一般是跟用户相关的一些设置,如断点 记录等,一般不用放到版本管理中。而project.pbxproj 是工程描述文件,描述了工程里的源码文件、schema设置等。它的格式是文本类型的plist(Info.plist是binary plist),里面是一个一个的object,具体的各种object定义可以参见文末给出的链接。

    project.pbxproj 的合并历来都是代码版本管理的噩梦。特别是当代码框架进行重构时,纯手工合并,简直就是不要不要的。如下面是两个工程文件的diff,大家感受下:

    处理前的工程文件对比

    眼一花,基本上就合并出错了,轻则工程少文件,重则把语法玩坏了,Xcode直接打不开了。

    分析

    pbxproj文件简要说明

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

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

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

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

    这里的BF3014CF1C10632C0080D38E 是uuid,而后面又是对象。objects中的对象都有一个isa字段,表明了object的类型,而object的其他字段取决于object的类型。

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

    这里对一些常用的类型,进行简要说明:

    • PBXFileReference

    PBXFileReference用来跟踪工程中使用的外部文件(对应到磁盘),包括源文件、头文件、资源文件、库、生成的应用文件等,它会被PBXGroup、PBXBuildFile等调用,如:

    BF30150E1C106FD70080D38E /* AAStable1ViewController.h */ = {
        isa = PBXFileReference; 
        fileEncoding = 4; 
        lastKnownFileType = sourcecode.c.h; 
        path = AAStable1ViewController.h; 
        sourceTree = "<group>"; 
    };
    BF30150F1C106FD70080D38E /* AAStable1ViewController.m */ = {
        isa = PBXFileReference; 
        fileEncoding = 4; 
        lastKnownFileType = sourcecode.c.objc; 
        path = AAStable1ViewController.m; 
        sourceTree = "<group>"; 
    };
    BF3014E51C10632C0080D38E /* Base */ = {
        isa = PBXFileReference; 
        lastKnownFileType = file.storyboard; 
        name = Base; path = Base.lproj/Main.storyboard; 
        sourceTree = "<group>"; 
    };
    
    • PBXBuildFile

    参与编译的PBXFileReference会有对应的PBXBuildFile,它会被PBXSourcesBuildPhase或PBXResourcesBuildPhase调用
    ,这里一般不会有.h文件,如

    BF3015101C106FD70080D38E /* AAStable1ViewController.m in Sources */ = {
        isa = PBXBuildFile; 
        fileRef = BF30150F1C106FD70080D38E /* AAStable1ViewController.m */;         
        settings = {ASSET_TAGS = (); }; 
    };
    BF3014E61C10632C0080D38E /* Main.storyboard in Resources */ = {
        isa = PBXBuildFile; 
        fileRef = BF3014E41C10632C0080D38E /* Main.storyboard */; 
    };
    
    • PBXSourcesBuildPhase

    编译过程,列出一些PBXBuildFile。如果有多个target,则会有多个source,如uitest、unit-test都会生成source,下面是主target的source,

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

    这个用来编译资源文件,如:

    BF3014D61C10632C0080D38E /* Resources */ = {
        isa = PBXResourcesBuildPhase;
        buildActionMask = 2147483647;
        files = (
            BF3014EB1C10632C0080D38E /* LaunchScreen.storyboard in Resources */,
            BF3014E81C10632C0080D38E /* Assets.xcassets in Resources */,
            BF3014E61C10632C0080D38E /* Main.storyboard in Resources */,
        );
        runOnlyForDeploymentPostprocessing = 0;
    };
    
    • PBXGroup

    对应工程中的group,如:

    BF3014DA1C10632C0080D38E /* PBTest */ = {
        isa = PBXGroup;
        children = (
            BF3014DE1C10632C0080D38E /* AppDelegate.h */,
            BF3014DF1C10632C0080D38E /* AppDelegate.m */,
            BF3014E41C10632C0080D38E /* Main.storyboard */,
            BF3014E71C10632C0080D38E /* Assets.xcassets */,
            BF3014E91C10632C0080D38E /* LaunchScreen.storyboard */,
            BF3014EC1C10632C0080D38E /* Info.plist */,
            BF3014DB1C10632C0080D38E /* Supporting Files */,
        );
        path = PBTest;
        sourceTree = "<group>";
    };
    

    另外,pbxproj中会把相同类型的object放在一起,并在前后添加注释,如:

    /* Begin PBXBuildFile section */
            BF3015101C106FD70080D38E /* AAStable1ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = BF30150F1C106FD70080D38E /* AAStable1ViewController.m */; settings = {ASSET_TAGS = (); }; };
            BF3014E01C10632C0080D38E /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = BF3014DF1C10632C0080D38E /* AppDelegate.m */; };
            BF3015131C106FF50080D38E /* AAStable2ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = BF3015121C106FF50080D38E /* AAStable2ViewController.m */; settings = {ASSET_TAGS = (); }; };
    /* End PBXBuildFile section */      
    

    常见的冲突

    根据我的多次合并经验,发现pbxproj文件冲突,主要是在跟文件相关的object的合并上。跟文件相关的object,主要就是上面具体描述的那几种类型:

    • PBXFileReference
    • PBXBuildFile
    • PBXSourcesBuildPhase
    • PBXResourcesBuildPhase
    • PBXGroup

    造成冲突的原因主要有:

    • 位置变化

    一般来说,除了PBXGroup 中文件是按实际的位置(比如在Xcode中的某个group中,把文件拉到前面的位置,那么它在pbxproj中的位置就在前面),其他的几个基本上跟文件的创建时间有关系,后面创建的文件,对应产生的PBXBuildFile 等对象就排在后面。

    但是,文件一多,再通过多人操作,PBXBuildFile 等对象的顺序往往就没规律了。如本文开头所举的示例中,虽然大多数object相同,但是由于它们在两边的位置不同,导致diff时比较困难。

    • 文件重命名,导致文件名不同

    在Xcode中对文件重命名后,相关的uuid并不会变化。只是对应的注释中的文件名发生变化。

    • 移动文件,导致uuid变化

    这里说的移动,指的是删除文件,并重新添加到工程。如项目重构时,可能要建立子目录,并把相应文件删除,并重新添加。移动文件后,对应的uuid肯定变了,但是注释中的文件名还是一样的。

    • 新增文件

    新增文件,会在PBXBuildFile 等分区中添加相应的对象。

    解决

    根据上面的分析,如果我们把容易造成冲突的对象进行重新排序,并把两边相同的对象放前面,然后是重命名或移动了的对象,最后是两边各自新增的对象,那么,后面再合并时,就要直观很多。

    所以,解决方法是使用脚本,把两个pbxproj文件进行上述的处理生成两个新的文件,然后再使用比较工具对两个新文件进行比较合并。

    regex come to rescure

    刚开始,考虑用plist的语法去解析,但是这样解析后再写回,会把文件中的注释搞没了。想起使用了无数次的正则表达式,最终考虑使用正则表达式来处理。

    考虑到我们工程一般很少用xib,所以PBXResourcesBuildPhase 就不做处理,PBXGroup 分组一般是每个人自己维护(如一个功能模块一个group),所以也不处理。最终的处理分三步,

    • 处理PBXBuildFile section 中的冲突
    • 处理PBXFileReference section 中的冲突
    • 处理PBXSourcesBuildPhase section 中的冲突

    每一步的处理,都是先匹配出section,然后在section中查找所有的对象,并把这些对象进行重新排序,最后把排序后的对象写回。

    用来匹配section的正则表达式有:

    gBuidFileSectionPattern = re.compile(r'''(?i)(.*/\* Begin PBXBuildFile section \*/\s+?)(.*?)(/\* End PBXBuildFile section \*/.*)''', re.S)
    gFileReferenceSectionPattern = re.compile(r'''(?i)(.*/\* Begin PBXFileReference section \*/\s+?)(.*?)(/\* End PBXFileReference section \*/.*)''', re.S)
    gSourceBuildPhaseSectionPattern = re.compile(r'''(?i)(.*/\* Begin PBXSourcesBuildPhase section \*/\s+?)(.*?)(/\* End PBXSourcesBuildPhase section \*/.*)''', re.S)
    

    用来匹配section中对象的正则如下:

    gBuidFilePattern = re.compile(r'''(?i)(^\s+(\w+) /\* (\S*)\s.*?$)''', re.S|re.M)
    gFileReferencePattern = gBuidFilePattern
    gSourceBuildPhaseSourcePattern = re.compile(r'''(^\s+(\w+?) /\* Sources \*/.*?$.*?^\s+files.*?$\n)(.*?)(^\s+\);.*?};\n)''', re.S|re.M)
    
    gSourceBuildPhaseFilePattern = gBuidFilePattern
    

    需要注意的是,对PBXSourcesBuildPhase的解析,由于PBXSourcesBuildPhase结构层级中多了一层,所以需要多一层正则去匹配处理。

    完整的代码见pbMerge.py,python正则表达式的使用,可以参考我之前写的python正则表达式

    经过脚本的处理后,本文开头的例子就变成这样,已经十分好合并了:

    预合并后工程文件的比较

    结论

    本文使用半自动方法,来对project.pbxproj文件的冲突进行解决。通过对该文件的预合并,使后面手动合并时更直观,同时极大地减少了工程文件合并出错,导致工程无法打开的问题。

    参考

    A brief look at the Xcode project format
    Xcode Project File Format
    http://www.zhihu.com/question/19763504/answer/14091247

    相关文章

      网友评论

      • 我叫阿水:楼主的意思是,先把这两个文件先复制出来diff跟merge,然后再代码合并,接着把预先merge好的文件直接覆盖冲突的那个?
      • Eveloson:PBXGroup section要合并咋办
      • 郑明明:吓到我了
      • zmj27404:感谢分享!!

      本文标题:Xcode中project.pbxproj合并冲突的解决

      本文链接:https://www.haomeiwen.com/subject/eydshttx.html