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

引言

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

project.pbxproj 的合并歷來都是代碼版本管理的噩夢。特別是當代碼框架進行重構(gòu)時,純手工合并,簡直就是不要不要的。如下面是兩個工程文件的diff,大家感受下:

處理前的工程文件對比

眼一花,基本上就合并出錯了,輕則工程少文件,重則把語法玩壞了,Xcode直接打不開了。

分析

pbxproj文件簡要說明

pbxproj是個plist文件,plist的格式跟json的差不多,就是一個個對象,對象是個字典,可以關(guān)聯(lián)一些字段和它的值。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中根據(jù)uuid和對象的關(guān)聯(lián),就可以唯一標識這個對象,方便對象的相互引用。如,通過uuid,PBXFileReference 類型的對象可以被PBXBuildFilePBXGroup對象引用,PBXBuildFile 對象可以被PBXSourcesBuildPhase 對象引用。

這里對一些常用的類型,進行簡要說明:

  • PBXFileReference

PBXFileReference用來跟蹤工程中使用的外部文件(對應(yīng)到磁盤),包括源文件、頭文件、資源文件、庫、生成的應(yīng)用文件等,它會被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會有對應(yīng)的PBXBuildFile,它會被PBXSourcesBuildPhase或PBXResourcesBuildPhase調(diào)用
,這里一般不會有.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

對應(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中會把相同類型的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 */      

常見的沖突

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

  • PBXFileReference
  • PBXBuildFile
  • PBXSourcesBuildPhase
  • PBXResourcesBuildPhase
  • PBXGroup

造成沖突的原因主要有:

  • 位置變化

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

但是,文件一多,再通過多人操作,PBXBuildFile 等對象的順序往往就沒規(guī)律了。如本文開頭所舉的示例中,雖然大多數(shù)object相同,但是由于它們在兩邊的位置不同,導(dǎo)致diff時比較困難。

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

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

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

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

  • 新增文件

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

解決

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

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

regex come to rescure

剛開始,考慮用plist的語法去解析,但是這樣解析后再寫回,會把文件中的注釋搞沒了。想起使用了無數(shù)次的正則表達式,最終考慮使用正則表達式來處理。

考慮到我們工程一般很少用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結(jié)構(gòu)層級中多了一層,所以需要多一層正則去匹配處理。

完整的代碼見pbMerge.py,python正則表達式的使用,可以參考我之前寫的python正則表達式。

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

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

結(jié)論

本文使用半自動方法,來對project.pbxproj文件的沖突進行解決。通過對該文件的預(yù)合并,使后面手動合并時更直觀,同時極大地減少了工程文件合并出錯,導(dǎo)致工程無法打開的問題。

參考

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)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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

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