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

引言

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

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

推荐阅读更多精彩内容

  • 引言 Xcode的工程文件是 工程名.xcodeproj,它其实是个package包,通过显示包内容,可以查看到它...
    好雨知时节浩宇阅读 9,070评论 15 12
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,517评论 18 139
  • Xcode工程文件project.pbxproj小结 简介 project.pbxproj 文件被包含于 Xcod...
    凌巅阅读 26,910评论 5 72
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,952评论 4 60
  • 人类之成一民族一国家者,亦各有其生命焉。 有青春之民族,斯有白首之民族,有青春之国家,斯有白首之国家。 吾之民族若...
    Jeff_bf40阅读 462评论 0 1