引言
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 = "";
};
//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 /* [email protected] in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
//5、PBXGroup
BF3014CF1C10632C0080D38E = {
isa = PBXGroup;
children = (
BF3014DA1C10632C0080D38E /* PBTest */,
BF3014F41C10632C0080D38E /* PBTestTests */,
BF3014FF1C10632D0080D38E /* PBTestUITests */,
BF3014D91C10632C0080D38E /* Products */,
);
sourceTree = "";
};
这里的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 = "";
};
//示例2
1A58A5391CE0407C0020DE69 /* MomoChatSDK.h */ = { (1次)
isa = PBXFileReference;
fileEncoding = 4;
lastKnownFileType = sourcecode.c.h;
path = MomoChatSDK.h; (1次)
sourceTree = "";
};
//示例3
1A58A5391CE0407C0020DE69 /* Base */ = {
isa = PBXFileReference;
lastKnownFileType = file.storyboard;
name = Base;
path = Base.lproj/Main.storyboard;
sourceTree = "";
};
......
/* 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 /* [email protected] 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 = "";
};
//子目录中的内容,使用单独的对象来展示
060D3DF61EC75917001A30BE /* VideoPalyerView */ = {
isa = PBXGroup;
children = (
060D3DF71EC75917001A30BE /* KRVideoPlayerControlView.h */,
060D3DF81EC75917001A30BE /* KRVideoPlayerControlView.m */,
);
path = VideoPalyerView;
sourceTree = "";
};
//子目录中的内容,使用单独的对象来展示
060D3DF91EC75917001A30BE /* VideoPlayerVC */ = {
isa = PBXGroup;
children = (
060D3DFA1EC75917001A30BE /* KRVideoPlayerController.h */,
060D3DFB1EC75917001A30BE /* KRVideoPlayerController.m */,
);
path = VideoPlayerVC;
sourceTree = "";
};
1A58A5351CE03FA70020DE69 /* ChatModule */ = {
isa = PBXGroup;
children = (
1A58A5351CE03FA70020DE69 /* MomoChatShareService.h */,
1A58A5351CE03FA70020DE69 /* MomoChatShareService.m */,(1次)
);
name = ChatModule;
sourceTree = "";
};
1A58A5391CE0407C0020DE69 /* MomoChatSDK */ = {
isa = PBXGroup;
children = (
1A58A5391CE0407C0020DE69 /* MomoChatSDK.h */,(1次)
1A58A5391CE0407C0020DE69 /* MomoChatSDK.m */,
);
name = ChatModule;
sourceTree = "";
};
......
/* 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相同,相同则保留,反之则删除。
自动化脚本
正在完善中,请期待...