Xcode中project.pbxproj合并沖突的解決

引言

Xcode的工程文件是 工程名.xcodeproj,而它其實(shí)是個(gè)package目錄,通過(guò)顯示包內(nèi)容,可以查看到它內(nèi)部主要有project.pbxprojxcuserdata。其中,xcuserdata 一般是跟用戶相關(guān)的一些設(shè)置,如斷點(diǎn) 記錄等,一般不用放到版本管理中。而project.pbxproj 是工程描述文件,描述了工程里的源碼文件、schema設(shè)置等。它的格式是文本類型的plist(Info.plist是binary plist),里面是一個(gè)一個(gè)的object,具體的各種object定義可以參見(jiàn)文末給出的鏈接。

project.pbxproj 的合并歷來(lái)都是代碼版本管理的噩夢(mèng)。特別是當(dāng)代碼框架進(jìn)行重構(gòu)時(shí),純手工合并,簡(jiǎn)直就是不要不要的。如下面是兩個(gè)工程文件的diff,大家感受下:

處理前的工程文件對(duì)比

眼一花,基本上就合并出錯(cuò)了,輕則工程少文件,重則把語(yǔ)法玩壞了,Xcode直接打不開(kāi)了。

分析

pbxproj文件簡(jiǎn)要說(shuō)明

pbxproj是個(gè)plist文件,plist的格式跟json的差不多,就是一個(gè)個(gè)對(duì)象,對(duì)象是個(gè)字典,可以關(guān)聯(lián)一些字段和它的值。pbxproj的總體框架如下:

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

其中objects就是主要的字段。它本身又是一個(gè)對(duì)象,里面包含了一個(gè)個(gè)的鍵值對(duì)。如下:

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

這里的BF3014CF1C10632C0080D38E 是uuid,而后面又是對(duì)象。objects中的對(duì)象都有一個(gè)isa字段,表明了object的類型,而object的其他字段取決于object的類型。

objects中根據(jù)uuid和對(duì)象的關(guān)聯(lián),就可以唯一標(biāo)識(shí)這個(gè)對(duì)象,方便對(duì)象的相互引用。如,通過(guò)uuid,PBXFileReference 類型的對(duì)象可以被PBXBuildFilePBXGroup對(duì)象引用,PBXBuildFile 對(duì)象可以被PBXSourcesBuildPhase 對(duì)象引用。

這里對(duì)一些常用的類型,進(jìn)行簡(jiǎn)要說(shuō)明:

  • PBXFileReference

PBXFileReference用來(lái)跟蹤工程中使用的外部文件(對(duì)應(yīng)到磁盤(pán)),包括源文件、頭文件、資源文件、庫(kù)、生成的應(yīng)用文件等,它會(huì)被PBXGroup、PBXBuildFile等調(diào)用,如:

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會(huì)有對(duì)應(yīng)的PBXBuildFile,它會(huì)被PBXSourcesBuildPhase或PBXResourcesBuildPhase調(diào)用
,這里一般不會(huì)有.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

編譯過(guò)程,列出一些PBXBuildFile。如果有多個(gè)target,則會(huì)有多個(gè)source,如uitest、unit-test都會(huì)生成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

這個(gè)用來(lái)編譯資源文件,如:

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

對(duì)應(yīng)工程中的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中會(huì)把相同類型的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 */      

常見(jiàn)的沖突

根據(jù)我的多次合并經(jīng)驗(yàn),發(fā)現(xiàn)pbxproj文件沖突,主要是在跟文件相關(guān)的object的合并上。跟文件相關(guān)的object,主要就是上面具體描述的那幾種類型:

  • PBXFileReference
  • PBXBuildFile
  • PBXSourcesBuildPhase
  • PBXResourcesBuildPhase
  • PBXGroup

造成沖突的原因主要有:

  • 位置變化

一般來(lái)說(shuō),除了PBXGroup 中文件是按實(shí)際的位置(比如在Xcode中的某個(gè)group中,把文件拉到前面的位置,那么它在pbxproj中的位置就在前面),其他的幾個(gè)基本上跟文件的創(chuàng)建時(shí)間有關(guān)系,后面創(chuàng)建的文件,對(duì)應(yīng)產(chǎn)生的PBXBuildFile 等對(duì)象就排在后面。

但是,文件一多,再通過(guò)多人操作,PBXBuildFile 等對(duì)象的順序往往就沒(méi)規(guī)律了。如本文開(kāi)頭所舉的示例中,雖然大多數(shù)object相同,但是由于它們?cè)趦蛇叺奈恢貌煌?,?dǎo)致diff時(shí)比較困難。

  • 文件重命名,導(dǎo)致文件名不同

在Xcode中對(duì)文件重命名后,相關(guān)的uuid并不會(huì)變化。只是對(duì)應(yīng)的注釋中的文件名發(fā)生變化。

  • 移動(dòng)文件,導(dǎo)致uuid變化

這里說(shuō)的移動(dòng),指的是刪除文件,并重新添加到工程。如項(xiàng)目重構(gòu)時(shí),可能要建立子目錄,并把相應(yīng)文件刪除,并重新添加。移動(dòng)文件后,對(duì)應(yīng)的uuid肯定變了,但是注釋中的文件名還是一樣的。

  • 新增文件

新增文件,會(huì)在PBXBuildFile 等分區(qū)中添加相應(yīng)的對(duì)象。

解決

根據(jù)上面的分析,如果我們把容易造成沖突的對(duì)象進(jìn)行重新排序,并把兩邊相同的對(duì)象放前面,然后是重命名或移動(dòng)了的對(duì)象,最后是兩邊各自新增的對(duì)象,那么,后面再合并時(shí),就要直觀很多。

所以,解決方法是使用腳本,把兩個(gè)pbxproj文件進(jìn)行上述的處理生成兩個(gè)新的文件,然后再使用比較工具對(duì)兩個(gè)新文件進(jìn)行比較合并。

regex come to rescure

剛開(kāi)始,考慮用plist的語(yǔ)法去解析,但是這樣解析后再寫(xiě)回,會(huì)把文件中的注釋搞沒(méi)了。想起使用了無(wú)數(shù)次的正則表達(dá)式,最終考慮使用正則表達(dá)式來(lái)處理。

考慮到我們工程一般很少用xib,所以PBXResourcesBuildPhase 就不做處理,PBXGroup 分組一般是每個(gè)人自己維護(hù)(如一個(gè)功能模塊一個(gè)group),所以也不處理。最終的處理分三步,

  • 處理PBXBuildFile section 中的沖突
  • 處理PBXFileReference section 中的沖突
  • 處理PBXSourcesBuildPhase section 中的沖突

每一步的處理,都是先匹配出section,然后在section中查找所有的對(duì)象,并把這些對(duì)象進(jìn)行重新排序,最后把排序后的對(duì)象寫(xiě)回。

用來(lái)匹配section的正則表達(dá)式有:

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)

用來(lái)匹配section中對(duì)象的正則如下:

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

需要注意的是,對(duì)PBXSourcesBuildPhase的解析,由于PBXSourcesBuildPhase結(jié)構(gòu)層級(jí)中多了一層,所以需要多一層正則去匹配處理。

完整的代碼見(jiàn)pbMerge.py,python正則表達(dá)式的使用,可以參考我之前寫(xiě)的python正則表達(dá)式。

經(jīng)過(guò)腳本的處理后,本文開(kāi)頭的例子就變成這樣,已經(jīng)十分好合并了:

預(yù)合并后工程文件的比較

結(jié)論

本文使用半自動(dòng)方法,來(lái)對(duì)project.pbxproj文件的沖突進(jìn)行解決。通過(guò)對(duì)該文件的預(yù)合并,使后面手動(dòng)合并時(shí)更直觀,同時(shí)極大地減少了工程文件合并出錯(cuò),導(dǎo)致工程無(wú)法打開(kāi)的問(wèn)題。

參考

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 引言 Xcode的工程文件是 工程名.xcodeproj,它其實(shí)是個(gè)package包,通過(guò)顯示包內(nèi)容,可以查看到它...
    好雨知時(shí)節(jié)浩宇閱讀 9,658評(píng)論 15 12
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,502評(píng)論 19 139
  • Xcode工程文件project.pbxproj小結(jié) 簡(jiǎn)介 project.pbxproj 文件被包含于 Xcod...
    凌巔閱讀 27,886評(píng)論 5 72
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫(kù)、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,022評(píng)論 4 61
  • 人類之成一民族一國(guó)家者,亦各有其生命焉。 有青春之民族,斯有白首之民族,有青春之國(guó)家,斯有白首之國(guó)家。 吾之民族若...
    Jeff_bf40閱讀 561評(píng)論 0 1

友情鏈接更多精彩內(nèi)容