Xcode
作为日常开发iOS程序的IDE,支持C
、C++
、Objective-C
、Swift
、Ruby
等语言进行编写。日常开发入口就是Xcode workspace
或者Xcode project
。
workspace
是一个Xcode
文档,它将项目和其他文件、project分组。一个workspace
可以包含任意数量的Xcode project
,以及资源文件(JSON、脚本、图片、视频等)。workspace
除了组织每个project
中的文件外,还提供了所包含项目及其目标之间的隐式和显式关系。
project
就是一个 Xcode
工程,它是实际管理工程下 targets
、源码、资源文件、framework
等。project
只是一个容器,本身是无法被编译的,所以每个 project
至少应该有一个可编译的 target
, target
下需要包含可编译的源码。
由上图可以简单看出workspace
和project
的关系:
workspace
里可以包含多个project
;project
里包含多个target
;configuration
即Xcode
中的Debug/Release
等工程配置;scheme
配置target
编译参数;target
即每次编译生成对应产物:app
或者framework
;在 Xcode
中能看见所有的公共配置信息都存在于 project.pbxproj
中。pbxproj
全拼是Project Builder Xcode Project
,它其实是我们熟悉的plist
文件的一种,但是它不像我们常用的plist
文件有着优越的可读性,由于历史原因它才被Xcode
一直保存下来。
pbxproj
中定义了target
、script
、文件、configuration
等之间的引用关系,我们看到的Xcode
项目布局实际上是可视化了pbxproj
。
pbxproj
主要包含跟文件相关的 BuildFile
,Group
和 FileReference
;跟编译相关的 BuildPhase
和 Build Configuration
(List);以及一些列 Target
和 TargetDependency
。
开发者比较关键的信息是配置应用PP文件、包名及APP 版本号。
MARKETING_VERSION = 2.4.2;
ONLY_ACTIVE_ARCH = YES;
OTHER_CODE_SIGN_FLAGS = "--deep";
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
"-lc++",
);
PRODUCT_BUNDLE_IDENTIFIER = com.shq5785.com;
PRODUCT_NAME = shq5785;
PROVISIONING_PROFILE_SPECIFIER = "ppFile";
project.pbxproj
文件包含于 Xcode
工程文件 *.xcodeproj
之中,存储着 Xcode
工程的各项配置参数。它本质上是一种旧风格的 Property List 文件,历史可追溯到 NeXT 的 OpenStep。其可读性不如 xml
和 json
,苹果却一直沿用至今,作为一家以创新闻名的公司可能这里剩下的就是情怀吧。
project.pbxproj
使用 UUID
作为交叉引用的索引,保证每个配置信息对象的唯一性。因为 UUID
根据机器硬件和时间戳生成,避免了多人在同一时间段操作修改工程文件带来的问题。也就是说工程中每项配置对象都有个唯一的 UUID
,然后其他配置对象想引用某个配置对象直接使用它的 UUID
即可。这就跟我们编程时使用指针指向某个对象的地址一样,其他对象的属性想引用它,只需要给属性传个指针地址就行了。
可以把整个文件的内容想象成一个字典,字典中的 Key
按照字典序来排列。字典的第一层级总共有 5 个键值对,Key
分别为:archiveVersion
,classes
,objectVersion
,objects
和 rootObject
。其中重要的 Key
是 objects
和 rootObject
。
所有的配置对象都放在 objects
对应的 Value
中,包括根对象(rootObject
)。 objects
对应的 Value
也是一个字典,Key 都为 UUID
,Value 依然是个字典。可以将 rootObject
的值(是一个 UUID
)作为 Key
在 objects
对应的字典中找到根对象。这个根对象的 isa
属性为 PBXProject
(isa = PBXProject
)。读懂 project.pbxproj
的最好方式就是顺着 rootObject
的各个属性对应的 UUID
在 objects
中找到对应的对象,然后一层层看下去。这样整个文件的配置信息存放方式就慢慢摸清了。
objects
的键值对根据内容类型被分成了若干个 section
,虽然 section
的顺序是 Xcode
私有 API 钦定的,但每个 section
内部的键值对会根据 Key 的字典序排列。采用注释的方式分节也使得可读性更强。section
的数量跟工程有关,尤其是每个工程的 BuildPhase
和 Target
差别都很大。
每个 section
中的对象类型都是相同的,对象的类型是靠 isa
的值区分的。对象内部的属性类型以及含义可以参照这篇文章提供的对照表: Xcode Project File Format
每个对象内部的属性(也是键值对)会把 isa
排在最前面,其余的按照字典序排列。
数组内部的顺序完全按照元素内容的字典序排列。
大概分为以下几类信息:
工程中的文件关联信息、资源关联信息
PBXBuildFile
参与编译的文件;
PBXFileReference
工程中的所有文件信息;
文件的组织结构信息
PBXGroup
工程中的文件夹;工程的编译配置、证书配置信息
PBXResourcesBuildPhase
编译阶段的资源配置;
PBXFrameworksBuildPhase
编译阶段的framework
配置;
PBXProject
工程信息;
PBXNativeTarget
工程中所有target的信息;
XCConfigurationList
每个target
下包含的编译模式,如Debug 、 Release等模式;
XCBuildConfiguration
具体的编译信息,如Release模式下的编译配置;
文件间的关系大致如下图所示:
每一项资源在这个文件中都有一个值作为唯一标识,如903C829A2075C24300EB9AD0。可以看做是id。
一般每个ID值后面都会有一个注释来进行说明这个ID对应的具体内容。如903C829C2075C24300EB9AD0 /* TLauncher */,
表示这个ID是代表的是TLauncher
这个target。
相同类型的资源是按段进行整理的。
每一段内容前用/* Begin xxx section */
注释作为开始。用/* End xxx section */
作为这一段内容的结束。
每一项内容中用isa = xxx
指示该资源所属于的类型。
其中,PBXFrameworksBuildPhase
、PBXResourcesBuildPhase
、PBXShellScriptBuildPhase
、PBXTargetDependency
、PBXSourcesBuildPhase
这几项内容都是对工程在编译阶段的配置。
下面是 objects
中 PBXNativeTarget section
的一个对象,感受一下格式:
/* Begin PBXNativeTarget section */
00E356ED1AD99517003FC87E /* shq5785Tests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "mrcsTests" */;
buildPhases = (
E813BE00D49641555FA7986E /* [CP] Check Pods Manifest.lock */,
00E356EA1AD99517003FC87E /* Sources */,
00E356EB1AD99517003FC87E /* Frameworks */,
00E356EC1AD99517003FC87E /* Resources */,
);
buildRules = (
);
dependencies = (
00E356F51AD99517003FC87E /* PBXTargetDependency */,
);
name = shq5785Tests;
productName = shq5785Tests;
productReference = 00E356EE1AD99517003FC87E /* shq5785Tests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
13B07F861A680F5B00A75B9A /* shq5785 */ = {
isa = PBXNativeTarget;
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "shq5785" */;
buildPhases = (
C8F5F5993094D81A94013E0D /* [CP] Check Pods Manifest.lock */,
FD10A7F022414F080027D42C /* Start Packager */,
13B07F871A680F5B00A75B9A /* Sources */,
13B07F8C1A680F5B00A75B9A /* Frameworks */,
13B07F8E1A680F5B00A75B9A /* Resources */,
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
605CC81222DD68EB009545B7 /* Embed Frameworks */,
3A54938F9F6AAF435223E489 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
dependencies = (
);
name = mrcs;
productName = mrcs;
productReference = 13B07F961A680F5B00A75B9A /* shq5785.app */;
productType = "com.apple.product-type.application";
};
2D02E47A1E0B4A5D006451C7 /* shq5785-tvOS */ = {
isa = PBXNativeTarget;
buildConfigurationList = 2D02E4BA1E0B4A5E006451C7 /* Build configuration list for PBXNativeTarget "shq5785-tvOS" */;
buildPhases = (
A6B45AA48E3A2F14663044B6 /* [CP] Check Pods Manifest.lock */,
FD10A7F122414F3F0027D42C /* Start Packager */,
2D02E4771E0B4A5D006451C7 /* Sources */,
2D02E4781E0B4A5D006451C7 /* Frameworks */,
2D02E4791E0B4A5D006451C7 /* Resources */,
2D02E4CB1E0B4B27006451C7 /* Bundle React Native Code And Images */,
);
buildRules = (
);
dependencies = (
);
name = "shq5785-tvOS";
productName = "shq5785-tvOS";
productReference = 2D02E47B1E0B4A5D006451C7 /* shq5785-tvOS.app */;
productType = "com.apple.product-type.application";
};
2D02E48F1E0B4A5D006451C7 /* shq5785-tvOSTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 2D02E4BB1E0B4A5E006451C7 /* Build configuration list for PBXNativeTarget "shq5785-tvOSTests" */;
buildPhases = (
D14E060D8F1725B6C5A79756 /* [CP] Check Pods Manifest.lock */,
2D02E48C1E0B4A5D006451C7 /* Sources */,
2D02E48D1E0B4A5D006451C7 /* Frameworks */,
2D02E48E1E0B4A5D006451C7 /* Resources */,
);
buildRules = (
);
dependencies = (
2D02E4921E0B4A5D006451C7 /* PBXTargetDependency */,
);
name = "shq5785-tvOSTests";
productName = "shq5785-tvOSTests";
productReference = 2D02E4901E0B4A5D006451C7 /* shq5785-tvOSTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
/* End PBXNativeTarget section */
可以根据 00E357021AD99517003FC87E
找到对应的 buildConfigurationList
对象的内容,所以说 project.pbxproj
使用 UUID
作为交叉引用的索引。通过这种关系,可以递归构建一张有向图,每个对象都是一个节点。
当更换证书信息时,可按照如下步骤实施。
首先,通过字符串匹配/* Begin PBXProject section */
,找到工程信息内容。
在该段内容中,通过查找targets
字段。找到该工程下的所有target
。通过字符串与注释进行匹配,找到需要打包的target
的ID值。
获取到目标target
的ID值之后。通过注释,筛选出PBXNativeTarget
内容段。
在PBXNativeTarget
内容段中,找到targetID
对应的内容。在这段内容中找到buildConfigurationList
字段,获取到这个target
下的编译配置列表对象ID。
此时,进入XCConfigurationList
内容段。在该范围中,通过上一步中的编译配置列表对象ID,获取该target下的配置模式列表内容。
在配置列表中,找到需要使用的模式。拿到该模式对应的配置对象ID值。
进入XCBuildConfiguration
编译配置段。通过上一步中的对象ID值,定位到该配置对象。
修改该配置对象中的内容。如可以通过修改PROVISIONING_PROFILE
等字段的值实现更换证书。
scheme
不是编译target
的必要条件,没有scheme
不影响Xcode
的编译操作,但是,没有scheme
就没办法在编译时传入参数条件,插入编译脚本,配置个性化编译配置,所以scheme
是Xcode
编译时的必须选项。
打开一个scheme
源文件,我们可以看到如下布局:
可以看到,最外层包含着build
、test
、launch
、profile
、analyze
、archive
。恰好对应了Xcode中的与之对应的命令,再次验证了Xcode
就是pbxproj
的可视化呈现。
进入BuildAction
可以看到在Xcode中添加的预编译脚本和各种环境变量配置,这些配置有的是在编译过程中必不可少的参数,有的是方便开发者管理编译产物的必须配置,灵活运用这些配置,可以让Xcode更好的为开发者服务。
target
用于指定要构建的产物,即framework
或者app
。target
只包含了当前project
中的部分指定的代码和资源文件,每一个target
只能构建出一个特定的构建产物,为了丰富构建产物,一个project
可以拥有多个target
。
target
使用Build Settings
和Build Phases
的形式来进行个性化配置,默认这些配置可以通过project
继承,也可以通过手动或者配置文件的方式覆盖其他配置。
target
之间可以互相依赖,如果是在同个workspace
下,Xcode
默认会触发隐式依赖,当然,如果用手动配置依赖关系,则会变为显式依赖。显式依赖的优先级高于隐式依赖。