本文目录

Xcode-project-edite

引子

通过「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 类的完整关系如下:

09-xcode-object-01-classes

Tips: 图中以 PBX 为前缀的类型才是 project.pbxproj 中存在的 Object。

Reference Attributes

前文已经介绍了基类 AbstractObject 和常规属性的修饰器 attribute, 它将常规属性的内容封装在 AbstractObjectAttribute 中,最终存入 @attributes 数组用于后续查询。

而 Xcode 项目文件中除了常规属性之外,还有 Object 的引用关系和 objects 字典需要进行保存,如 PBXProject 对象就同时保存了对 mainGrouptargets 的引用,另外它们都需要在 objects 字典中有记录。在 Xcodeproj 中这些引用关系是通过特殊的对象来记录的。

如果按引用是否存在多份不同对象来分类,可分喂为:

  • 单一引用关系: 如项目 PBXProject 只会有一个 mainGroup 作为其主文件夹,另外每个 PBXBuildFile 也只会有一份 PBXFileReference 引用,它们之间是单一直接引用关系。
  • 多引用: 如 PBXNativeTarget 可以存在多个构建规则和依赖:PBXBuildRuleAbstractBuildPhase, 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

从这里的逻辑可以分为三部分,同常规属性修饰器实现一致:

  1. 创建 AbstractObjectAttribute 用于记录属性类型 isa ,以及对应的值或者关联对象的 classes;
  2. 通过 attr_reader 定义了名称为 attrb.name 的 getter 方法;
  3. 通过 define_method 定义了名称为 attrb.name= 的 setter 方法;

has_one 本质是通过 attr_reader 来获取属性的值,而 setter 则通过 define_method 动态添加。借用 Ruby runtime 能力,通过 instance_variable_set 将引用对象保存到对应的实例变量中。自定义 setter 主要为了处理新旧对象的引用关系,通过基类 AbstractObject 对象提供的 add_referrerremove_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_referrerremove_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 是可以存在依赖关系。

举个例子:

09-xcode-object-02-nest-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_referrerremove_referrer 来更新引用计数。具体我们在下一节展开。

Object Configuration

前文整体介绍了项目文件 project.pbxproj 的解析,而对于 Object 的解析方法 Object::configure_with_plist 并未展开。因为它涉及到本文的两个重要类型 ObjectListObjectDictionary,为了方便理解,我们在这里来详细分析。

首先,回顾一下 project.pbxproj 解析 flow:

02-parse-project

可以看到方法 Object::objects_by_uuid 会调用 Project::new_from_plist 完成 object 的遍历,直至 objects 解析完成。

configure_with_plist

让我们将 Object::configure_with_plist 展开。

09-xcode-object-05-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
  1. 根据 uuid 获取 project.pbxproj object 对应的 plist 字典 object_plist;
  2. 遍历 simple_attributes,将 object_plist 中与常规属性名一致的值存入对应的 attribute 中,并对 object_plist 中的值清除,避免无限赋值;
  3. 单一引用的 Object 解析,遍历 to_one_attributes,从 object_plist 中取出对应的字典 ref_uuid 以生成 object 并绑定到当前对象。
  4. 多引用关系的 Object 解析,遍历 to_many_attributes,从 object_plist 中取出对应的数组 ref_uuids,再遍历生成 Objects 存入 attrbute 的 list 中。
  5. 多引用关系的 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
  1. 以 isa 字符串获取 Xcodeproj 中对应的 Xcode Object 类型 klass,以初始化 object;
  2. 更新 rootObject 的 objects 表 objects_by_uuid,另外仅当 root_object 为 true 时,才会通过 add_referrer 将 project 与 root_object 关联;
  3. 遍历 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_referrerremove_referrer 来完成。而该方法除了 has_one 属性会修改之外,ObjectListObjectDictionary 容器均提供了对应的接口。

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。因此,我们要做的事情就是保证引用属性被修改时,对应的 referresobjects_by_uuid 的同步。同步逻辑就封装在 has_onehas_manyhas_many_references_by_keys 这些引用修饰器中。

09-xcode-object-05-add_referrer

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

PBXGroupchildren 属性为例,它就是通过 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 的是其储存的元素为 ObjectDictionaryObjectDictionary 通过重载 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_refproduct_group。想要以 CLI 的方式添加 Xcode 子项目依赖,就需要构造出 projectReferences 字典。

具体步骤说明如下:

0x01 新建子工程文件引用

通过 new_file_reference 创建 PBXReference 以指向子项目的工程文件地址。

0x02 新建子工程 Product 引用

通过 projectReferences 属性名可知,当我们添加 Xcode 子项目作为依赖时,本质上是将子项目的产物,以 framework 形式加进主工程。为此我们需要一个 Proxy 来指向该产物的 Xcode Object Identify,即 GUID。

完整引用关系如下:

09-xcode-object-06-product-group-ref

  1. 首先需要新建一个 PBXGroup 作为 product group reference。
  2. 新建 PBXReferenceProxyPBXContainerItemProxy 代理来桥接子项目的 GUID,对应为 container 代理的 remote_global_id_string 字段。同时还需要记录 container_portal,其值为子项目 Product Group 的 GUID。

0x03 写入 project_references

新建 Object Dictionary 将前两步生成的 ref 分别记录在 project_refproduct_group,最后写入 rootObject 的 project_references 中。

总结

xcodeproj 利用属性修饰器不仅完成了对 Xcode 项目文件的映射,同时也支持了对 Xcode 项目文件的编辑。并且作者利用 AbstractObjectAttribute 和 ObjectList 抹除了不同类型的 Xcode Object 差异,为批量解析提供了便利。最后为了支持嵌套的 Xcode 工程,引入 ObjectDictionary 的同时仍旧保证了现有结构的完整性。另外从嵌套的 Xcode 工程,我们也能理解为何 Xcode 项目文件需要提供 GUID,毕竟,避免键值的冲突对于大型项目而言尤其重要。

知识点问题梳理

这里罗列了五个问题用来考察你是否已经掌握了这篇文章,如果没有建议你加入收藏再次阅读:

  1. 描述一下 has_many_references_by_keys 的实现和作用?
  2. has_one 修饰的属性为何最终也声明为 ObjectList 类型?
  3. 说说 xcodeproj 是如何保证全局 objects 索引的一致性?
  4. 说说 PBXReferenceProxyPBXContainerItemProxy 的作用?
  5. 描述一下如何通过 CLI 添加 Xcode 子项目?