引言
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。也就是我們開發時劃分的目錄,
例如:工程中的目錄結構如下圖所示:
對應的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相同,相同則保留,反之則刪除。
自動化腳本
正在完善中,請期待...