本文目录
引子
通过「Xcode 工程文件解析」一文,我们了解到 project.pbxproj
文件的重要性,Xcode 正是通过它来管理项目中的各种源代码、脚本文件、资源文件、依赖库等。也深入的分析了 project.pbxproj
文件的组成部分,而 xcodeproj
正是通过 Ruby 脚本来编辑该文件,从而将 Pod 依赖库添加进 Xcode 项目中。本文将会继续深入对 xcodeproj
的剖析。
Object Attributes
前文中我们提过 xcodeproj
通过 Ruby 的 attr_accessor 特性,实现了一套对 project.pbxproj
文件结构的映射,将 Sections Object 中的 isa 字段作为类名,映射为对应的 Object 类。
在 xcodeproj
中 Object 类的完整关系如下:
Tips: 图中以
PBX
为前缀的类型才是 project.pbxproj 中存在的 Object。
Reference Attributes
前文已经介绍了基类 AbstractObject
和常规属性的修饰器 attribute
, 它将常规属性的内容封装在 AbstractObjectAttribute
中,最终存入 @attributes
数组用于后续查询。
而 Xcode 项目文件中除了常规属性之外,还有 Object 的引用关系和 objects 字典需要进行保存,如 PBXProject
对象就同时保存了对 mainGroup
和 targets
的引用,另外它们都需要在 objects 字典中有记录。在 Xcodeproj
中这些引用关系是通过特殊的对象来记录的。
如果按引用是否存在多份不同对象来分类,可分喂为:
- 单一引用关系: 如项目
PBXProject
只会有一个mainGroup
作为其主文件夹,另外每个PBXBuildFile
也只会有一份PBXFileReference
引用,它们之间是单一直接引用关系。 - 多引用: 如
PBXNativeTarget
可以存在多个构建规则和依赖:PBXBuildRule
、AbstractBuildPhase
,PBXTargetDependency
等。
单一引用关系
has_one 修饰器
Defines a new relationship to a single and synthesises the corresponding methods.
has_one
用于定义 Objects 间唯一引用关系,它会生成对应的 Access 方法来维护引用关系。
1def has_one(singular_name, isas)
2 isas = [isas] unless isas.is_a?(Array)
3 # 1. 创建 AbstractObjectAttribute
4 attrb = AbstractObjectAttribute.new(:to_one, singular_name, self)
5 attrb.classes = isas
6 add_attribute(attrb)
7
8 # 2. 添加名为 `attrb.name` 的 Getter 方法
9 attr_reader(attrb.name)
10 # 1.9.2 fix, see https://github.com/CocoaPods/Xcodeproj/issues/40.
11 public(attrb.name)
12
13 # 3. 添加名为 `attrb.name=` 的 Setter 方法
14 variable_name = :"@#{attrb.name}"
15 define_method("#{attrb.name}=") do |value|
16 attrb.validate_value(value)
17
18 previous_value = send(attrb.name)
19 return value if previous_value == value
20 mark_project_as_dirty!
21 previous_value.remove_referrer(self) if previous_value
22 instance_variable_set(variable_name, value)
23 value.add_referrer(self) if value
24 end
25end
从这里的逻辑可以分为三部分,同常规属性修饰器实现一致:
- 创建
AbstractObjectAttribute
用于记录属性类型isa
,以及对应的值或者关联对象的 classes; - 通过
attr_reader
定义了名称为attrb.name
的 getter 方法; - 通过
define_method
定义了名称为attrb.name=
的 setter 方法;
has_one 本质是通过 attr_reader
来获取属性的值,而 setter 则通过 define_method
动态添加。借用 Ruby runtime 能力,通过 instance_variable_set
将引用对象保存到对应的实例变量中。自定义 setter 主要为了处理新旧对象的引用关系,通过基类 AbstractObject
对象提供的 add_referrer
与 remove_referrer
来更新全局的 objects 表。
再补充一点关于参数 isas,他会检查参数类型,保证类型 isas 为数组。
1isas = [isas] unless isas.is_a?(Array)
而对于非 Array 类型则会手动创建 Array 来封装。Why ?为了解决 project.pbxproj
中一个属性可以支持多种不同 isa 类型。举个例子,如 PBXBuildFile
的 file_ref:
1has_one :file_ref, [
2 PBXFileReference,
3 PBXGroup,
4 PBXVariantGroup,
5 XCVersionGroup,
6 PBXReferenceProxy,
7]
而对于仅支持一种 isa 类型的属性,如:
1has_one :build_configuration_list, XCConfigurationList
像这样声明的 build_configuration_list
属性,背后也是通过数组:[XCConfigurationList]
来保存引用对象的。
多引用关系
has_many
has_many 用于定义一组有序的引用关系。它仅提供了 reader 方法。
1def has_many(plural_name, isas)
2 isas = [isas] unless isas.is_a?(Array)
3 # 1. 创建 AbstractObjectAttribute
4 attrb = AbstractObjectAttribute.new(:to_many, plural_name, self)
5 attrb.classes = isas
6 add_attribute(attrb)
7
8 # 2. 添加名为 `attrb.name` 的 Getter 方法
9 variable_name = :"@#{attrb.name}"
10 define_method(attrb.name) do
11 # Here we are in the context of the instance
12 list = instance_variable_get(variable_name)
13 unless list
14 list = ObjectList.new(attrb, self)
15 instance_variable_set(variable_name, list)
16 end
17 list
18 end
19end
这里未提供 setter 方法,由于 has_many
声明创建的 ObjectList 对象作为容器,来记录关联对象。因此引用对象的变更是对容器元素的操作。
接着来看实现:
第一步也是创建并保存 AbstractObjectAttribute
。getter 则比较简单,以惰性初始化的方式返回 ObjectList
数组。作为 Array 的子类 ObjectList
也提供了 add_referrer
与 remove_referrer
来更新关联对象的全局引用关系,后续会展开。
has_many 使用如下:
1class PBXNativeTarget < AbstractTarget
2 # @return [PBXBuildRule] the build rules of this target.
3 has_many :build_rules, PBXBuildRule
4end
5
6class PBXGroup < AbstractTarget
7 # @return [PBXBuildRule] the build phases of the target.
8 has_many :children, [PBXGroup, PBXFileReference, PBXReferenceProxy]
9end
has_many_references_by_keys
has_many_references_by_keys
同样为容器类引用,也是通过 ObjectList 来存储关联对象。不过它用于记录 project 之间的引用关系,我们知道在 Xcode 中 Project 是可以存在依赖关系。
举个例子:
Example.xcodeproj 通过 Add Files to Example
方式直接将 A.xcodeproj 添加至该项目中,以方便我们同事管理多个项目。此时 project.pbxproj
文件则会增加一个字典 projectReferences 来记录该引用关系:
1/* Begin PBXProject section */
2 85BB2A4F26DA8CB600AE6943 /* Project object */ = {
3 isa = PBXProject;
4 mainGroup = 85BB2A4E26DA8CB600AE6943;
5 ...
6 projectReferences = (
7 {
8 ProductGroup = 85BB2A9126DA916100AE6943 /* Products */;
9 ProjectRef = 85BB2A9026DA916100AE6943 /* A.xcodeproj */;
10 },
11 );
12 };
13/* End PBXProject section */
另外需要注意的是,由于引用的是一个完整的 Project,这里的 ProjectRef
指向的是一个 PBXFileReference:
185BB2A9026DA916100AE6943 /* A.xcodeproj */ = {
2 isa = PBXFileReference;
3 lastKnownFileType = "wrapper.pb-project";
4 name = A.xcodeproj;
5 path = A/A.xcodeproj;
6 sourceTree = "<group>";
7};
这也给我们一个提示,大型工程的功能代码该如何高效组织。恰好 CocoaPods 在 1.7 版本中提供的 install feature:generate_multiple_pod_projects,为 Pods 库生成多 Project 工程,则是他们提供的解决方案。
这里简单介绍一下背景:
从历史上看,CocoaPods 总是生成一个 Pods.xcodeproj,其中包含项目编译所需的所有目标和构建设置。对于较小的项目,仅使用一个包含整个 Podfile 的项目就可以了;但是,随着项目的增长,Pods.xcodeproj 文件的大小也会增加。
Pods.xcodeproj 文件越大,Xcode 解析其内容所需的时间就越长,这会导致 Xcode 体验下降。通过将每个 pod 集成为自己独立的 Xcode project 并嵌套在顶级 Pods.xcodeproj 下,从而为更大的 CocoaPods 项目带来了一些显着的性能改进。
此外,在大型代码库中,此功能可能特别有用,因为开发人员可以选择仅打开他们需要处理的特定 .xcodeproj(位于 Pods/ 目录下),而不是打开整个工作区,这会减慢他们的开发过程。
回到本文中,has_many_references_by_key
就是用于记录 projectReferences
,其实现如下:
1def has_many_references_by_keys(plural_name, classes_by_key)
2 attrb = AbstractObjectAttribute.new(:references_by_keys, plural_name, self)
3 attrb.classes = classes_by_key.values
4 attrb.classes_by_key = classes_by_key
5 add_attribute(attrb)
6
7 # Getter 实现同 has_many, 通过 ObjectList 来存储
8 ...
9end
该方法用于修饰 PBXProject
属性来关联其他项目文件,声明如下:
1class PBXProject < AbstractObject
2 # @return [Array<ObjectDictionary>] any reference to other projects.
3 has_many_references_by_keys :project_references,
4 :project_ref => PBXFileReference,
5 :product_group => PBXGroup
6end
而存储的元素为 ObjectDictionary
,所记录的键值对为:
1{
2 :project_ref => PBXFileReference,
3 :product_group => PBXGroup
4}
同 ObjectList
类似,ObjectDictionary
作为 Hash 的子类也提供了 add_referrer
与 remove_referrer
来更新引用计数。具体我们在下一节展开。
Object Configuration
前文整体介绍了项目文件 project.pbxproj
的解析,而对于 Object 的解析方法 Object::configure_with_plist 并未展开。因为它涉及到本文的两个重要类型 ObjectList
与 ObjectDictionary
,为了方便理解,我们在这里来详细分析。
首先,回顾一下 project.pbxproj
解析 flow:
可以看到方法 Object::objects_by_uuid
会调用 Project::new_from_plist
完成 object 的遍历,直至 objects 解析完成。
configure_with_plist
让我们将 Object::configure_with_plist
展开。
内容主要分 5 个部分:
1def configure_with_plist(objects_by_uuid_plist)
2 #1. 根据 uuid 获取 object 的属性字典
3 object_plist = objects_by_uuid_plist[uuid].dup
4
5 unless object_plist['isa'] == isa
6 raise "[Xcodeproj] Attempt to initialize `#{isa}` from plist with " \
7 "different isa `#{object_plist}`"
8 end
9 object_plist.delete('isa')
10 #2. 常规属性解析
11 simple_attributes.each do |attrb|
12 attrb.set_value(self, object_plist[attrb.plist_name])
13 object_plist.delete(attrb.plist_name)
14 end
15 #3. 单一引用关系的属性解析
16 to_one_attributes.each do |attrb|
17 ref_uuid = object_plist[attrb.plist_name]
18 if ref_uuid
19 ref = object_with_uuid(ref_uuid, objects_by_uuid_plist, attrb)
20 attrb.set_value(self, ref) if ref
21 end
22 object_plist.delete(attrb.plist_name)
23 end
24 #4. 多引用关系的 Object 解析
25 to_many_attributes.each do |attrb|
26 ref_uuids = object_plist[attrb.plist_name] || []
27 list = attrb.get_value(self)
28 ref_uuids.each do |uuid|
29 ref = object_with_uuid(uuid, objects_by_uuid_plist, attrb)
30 list << ref if ref
31 end
32 object_plist.delete(attrb.plist_name)
33 end
34 #5. 多引用关系的 Project 解析
35 references_by_keys_attributes.each do |attrb|
36 hashes = object_plist[attrb.plist_name] || {}
37 list = attrb.get_value(self)
38 hashes.each do |hash|
39 dictionary = ObjectDictionary.new(attrb, self)
40 hash.each do |key, uuid|
41 ref = object_with_uuid(uuid, objects_by_uuid_plist, attrb)
42 dictionary[key] = ref if ref
43 end
44 list << dictionary
45 end
46 object_plist.delete(attrb.plist_name)
47 end
48
49 unless object_plist.empty?
50 UI.warn "[!] Xcodeproj doesn't know about the following " \
51 "attributes #{object_plist.inspect} for the '#{isa}' isa." \
52 "\nIf this attribute was generated by Xcode please file " \
53 'an issue: https://github.com/CocoaPods/Xcodeproj/issues/new'
54 end
55end
- 根据 uuid 获取
project.pbxproj
object 对应的 plist 字典 object_plist; - 遍历
simple_attributes
,将 object_plist 中与常规属性名一致的值存入对应的 attribute 中,并对 object_plist 中的值清除,避免无限赋值; - 单一引用的 Object 解析,遍历
to_one_attributes
,从 object_plist 中取出对应的字典 ref_uuid 以生成 object 并绑定到当前对象。 - 多引用关系的 Object 解析,遍历
to_many_attributes
,从 object_plist 中取出对应的数组 ref_uuids,再遍历生成 Objects 存入 attrbute 的 list 中。 - 多引用关系的 Project 解析,遍历
references_by_keys_attributes
,从 object_plist 中取出对应的字典 hash,再遍历 hash 键值对生成项目引用对象,存入 attrbute 的 list 中。
object_with_uuid
上一节中,我们看到所有的引用属性的解析,都会调用 object_with_uuid,而该方法本身是缓存方法,最终记录了 rootObject 的 objects 表,key 为 uuid。该方法实现如下:
1def object_with_uuid(uuid, objects_by_uuid_plist, attribute)
2 unless object = project.objects_by_uuid[uuid] || project.new_from_plist(uuid, objects_by_uuid_plist)
3 UI.warn "`#{inspect}` attempted to initialize an object with an unknown UUID. "
4 ...
5 end
6 object
7rescue NameError
8 attributes = objects_by_uuid_plist[uuid]
9 raise "`#{isa}` attempted to initialize an object with unknown ISA "
10 ...
11end
首先从 project 的 objects 表 objects_by_uuid 中查询 object,如不存在,则会通过 Project::new_from_plist
解析出对应的 object 并存入 objects 表中。
1def new_from_plist(uuid, objects_by_uuid_plist, root_object = false)
2 attributes = objects_by_uuid_plist[uuid]
3 if attributes
4 #1.
5 klass = Object.const_get(attributes['isa'])
6 object = klass.new(self, uuid)
7 #2.
8 objects_by_uuid[uuid] = object
9 object.add_referrer(self) if root_object
10 #3.
11 object.configure_with_plist(objects_by_uuid_plist)
12 object
13 end
14end
- 以 isa 字符串获取 Xcodeproj 中对应的 Xcode Object 类型 klass,以初始化 object;
- 更新 rootObject 的 objects 表 objects_by_uuid,另外仅当 root_object 为 true 时,才会通过
add_referrer
将 project 与 root_object 关联; - 遍历 objects 字典以递归方式完成 objects 对象的初始化,并将 ISA 数据到 Xcode Object 的映射;
我们知道 xcodeproj 提供的是对 project.pbxproj
的编辑能力,因此,对于 objects_by_uuid
的更新不能仅停留在对 project.pbxproj
的解析。当用户编辑引用对象时也需要保证 objects_by_uuid
的一致性。
那么如何保证呢 ?答案就是通过 add_referrer
。
全局 Objects 索引
本节我们就来看看核心的 objects 表 objects_by_uuid
如何更新的。除了 new_from_plist
初始化 object 时主动更新 objects_by_uuid 之外,剩下的就是通过 add_referrer
与 remove_referrer
来完成。而该方法除了 has_one
属性会修改之外,ObjectList
和 ObjectDictionary
容器均提供了对应的接口。
add_referrer
1class AbstractObject
2 attr_reader :referrers
3
4 def add_referrer(referrer)
5 @referrers << referrer
6 @project.objects_by_uuid[uuid] = self
7 end
8
9 def remove_referrer(referrer)
10 @referrers.delete(referrer)
11 if @referrers.count == 0
12 mark_project_as_dirty!
13 @project.objects_by_uuid.delete(uuid)
14 end
15 end
16end
下面我们重点聊 add_referrer
,删除引用的逻辑同 add_referrer
类似就不展开了。
add_referrer
实现就两行,用于记录 会通知 object 另一个对象正在引用它,并将其记录到 referrers
中。如果 object 之前没有引用,则将其添加到 objects_by_uuid
表中。
前面提到 Xcodeproj
支持编辑 project.pbxproj
,而对 project.pbxproj
的编辑本质上是修改 objects 属性与维护 objects_by_uuid
。因此,我们要做的事情就是保证引用属性被修改时,对应的 referres
和 objects_by_uuid
的同步。同步逻辑就封装在 has_one
、has_many
、 has_many_references_by_keys
这些引用修饰器中。
has_one
has_one
直接通过在定义的 attribute setter 方法中调用 value.add_referrer(self) if value
来完成引用更新。注意,它更新前会先调用 remove_referrer
清除前值的引用关系。
has_many
对于数组引用类型,通过重载了 Array 的增加和删除方法来更新引用。
我们在前文示例代码中提到过,如果想要将新增的文件添加在 Xcode 项目中,需要通过 new_file_reference
将文件所对应的 PBXFileReference
添加至 PBXGroup
下,即保存在 children
属性中。
1def new_file_reference(group, path, source_tree)
2 path = Pathname.new(path)
3 ref = group.project.new(PBXFileReference)
4 group.children << ref
5 GroupableHelper.set_path_with_source_tree(ref, path, source_tree)
6 ref.set_last_known_file_type
7 ref
8end
以 PBXGroup
的 children
属性为例,它就是通过 ObjectList
的 <<
方法来添加 PBXFileReference
。
1def <<(object) ... end
2def +(object) ... end
3def insert(index, object) ... end
4def unshift(object) ... end
这几个增加 object 方法,均通过 perform_additions_operations
方法完成 object 的 add_referrer
方法调用。
1def perform_additions_operations(objects)
2 objects = [objects] unless objects.is_a?(Array)
3 objects.each do |obj|
4 owner.mark_project_as_dirty!
5 obj.add_referrer(owner)
6 attribute.validate_value(obj) unless obj.is_a?(ObjectDictionary)
7 end
8end
has_many_references_by_keys
has_many_references_by_keys
不同于 has_many
的是其储存的元素为 ObjectDictionary
,ObjectDictionary
通过重载 Hash 的更新方法来更新引用:
1def []=(key, object)
2 key = normalize_key(key)
3 if object
4 perform_additions_operations(object, key)
5 else
6 perform_deletion_operations(self[key])
7 end
8 super(key, object)
9end
10
11def perform_additions_operations(object, key)
12 owner.mark_project_as_dirty!
13 object.add_referrer(owner)
14 attribute.validate_value_for_key(object, key)
15end
在 perform_additions_operations
中,同样调用 object 的 add_referrer
完成引用对象都更新。
通过 CLI 添加 Xcode 子项目依赖
最后一节,我们通过命令行添加 Xcode 子项目,来实践一下前面提到的知识点。
要添加 Xcode 子项目依赖,本质也是添加文件引用,因此需要先调用 new_reference
。当新建引用文件类型为 .xcodeproj
时,会调用 new_subproject
。正如前文提到,在 Xcode 工程中添加普通文件也是通过 new_reference
来分发。
1def new_reference(group, path, source_tree)
2 ref = case File.extname(path).downcase
3 when '.xcdatamodeld'
4 new_xcdatamodeld(group, path, source_tree)
5 when '.xcodeproj'
6 new_subproject(group, path, source_tree)
7 else
8 new_file_reference(group, path, source_tree)
9 end
10
11 configure_defaults_for_file_reference(ref)
12 ref
13end
14
15def new_subproject(group, path, source_tree)
16 #1. 新建 sub project 文件引用
17 ref = new_file_reference(group, path, source_tree)
18 ref.include_in_index = nil
19
20 #2.1 新建 sub product groups 分组
21 product_group_ref = find_products_group_ref(group, true)
22
23 #2.2 遍历子项目的 objects 表,将其 product framework 作为当前项目的的产物代理
24 subproj = Project.open(path)
25 subproj.products_group.files.each do |product_reference|
26 container_proxy = group.project.new(PBXContainerItemProxy)
27 container_proxy.container_portal = ref.uuid
28 container_proxy.proxy_type = Constants::PROXY_TYPES[:reference]
29 container_proxy.remote_global_id_string = product_reference.uuid
30 container_proxy.remote_info = 'Subproject'
31
32 reference_proxy = group.project.new(PBXReferenceProxy)
33 extension = File.extname(product_reference.path)[1..-1]
34 reference_proxy.file_type = Constants::FILE_TYPES_BY_EXTENSION[extension]
35 reference_proxy.path = product_reference.path
36 reference_proxy.remote_ref = container_proxy
37 reference_proxy.source_tree = 'BUILT_PRODUCTS_DIR'
38
39 product_group_ref << reference_proxy
40 end
41 #4. 创建 ObjectDictionary,将 subproject 的文件引用和产物引用记录其中,最后将 ObjectDictionary 存入 project_references
42 attribute = PBXProject.references_by_keys_attributes.find { |attrb| attrb.name == :project_references }
43 project_reference = ObjectDictionary.new(attribute, group.project.root_object)
44 project_reference[:project_ref] = ref
45 project_reference[:product_group] = product_group_ref
46 group.project.root_object.project_references << project_reference
47
48 ref
49end
我们知道当 Xcode 项目中存在子项目时,Xcode 工程文件中将多出字典 projectReferences
,它包含两个 key project_ref
与 product_group
。想要以 CLI 的方式添加 Xcode 子项目依赖,就需要构造出 projectReferences
字典。
具体步骤说明如下:
0x01 新建子工程文件引用
通过 new_file_reference
创建 PBXReference
以指向子项目的工程文件地址。
0x02 新建子工程 Product 引用
通过 projectReferences
属性名可知,当我们添加 Xcode 子项目作为依赖时,本质上是将子项目的产物,以 framework 形式加进主工程。为此我们需要一个 Proxy 来指向该产物的 Xcode Object Identify,即 GUID。
完整引用关系如下:
- 首先需要新建一个
PBXGroup
作为 product group reference。 - 新建
PBXReferenceProxy
和PBXContainerItemProxy
代理来桥接子项目的 GUID,对应为 container 代理的remote_global_id_string
字段。同时还需要记录 container_portal,其值为子项目 Product Group 的 GUID。
0x03 写入 project_references
新建 Object Dictionary 将前两步生成的 ref 分别记录在 project_ref
和 product_group
,最后写入 rootObject 的 project_references
中。
总结
xcodeproj
利用属性修饰器不仅完成了对 Xcode 项目文件的映射,同时也支持了对 Xcode 项目文件的编辑。并且作者利用 AbstractObjectAttribute 和 ObjectList 抹除了不同类型的 Xcode Object 差异,为批量解析提供了便利。最后为了支持嵌套的 Xcode 工程,引入 ObjectDictionary 的同时仍旧保证了现有结构的完整性。另外从嵌套的 Xcode 工程,我们也能理解为何 Xcode 项目文件需要提供 GUID,毕竟,避免键值的冲突对于大型项目而言尤其重要。
知识点问题梳理
这里罗列了五个问题用来考察你是否已经掌握了这篇文章,如果没有建议你加入收藏再次阅读:
- 描述一下
has_many_references_by_keys
的实现和作用? has_one
修饰的属性为何最终也声明为 ObjectList 类型?- 说说
xcodeproj
是如何保证全局 objects 索引的一致性? - 说说
PBXReferenceProxy
与PBXContainerItemProxy
的作用? - 描述一下如何通过 CLI 添加 Xcode 子项目?