4

9. Xcode 工程文件编辑

 3 years ago
source link: https://looseyi.github.io/post/sourcecode-cocoapods/09-cocoapods-xcode-object/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

通过「Xcode 工程文件解析」一文,我们了解到 project.pbxproj 文件的重要性,Xcode 正是通过它来管理项目中的各种源代码、脚本文件、资源文件、依赖库等。也深入的分析了 project.pbxproj 文件的组成部分,而 xcodeproj 正是通过 Ruby 脚本来编辑该文件,从而将 Pod 依赖库添加进 Xcode 项目中。本文将会继续深入对 xcodeproj 的剖析。

Object Attributes

前文中我们提过 xcodeproj 通过 Ruby 的 Attribute 特性,实现了一套对 project.pbxproj 文件结构的映射,将 Sections Object 中的 isa 字段作为类名,映射为对应的 Object 类。

xcodeproj 中 Object 类的完整关系如下:

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 方法来维护引用关系。

def has_one(singular_name, isas)
	isas = [isas] unless isas.is_a?(Array)
   # 1. 创建 AbstractObjectAttribute 
	attrb = AbstractObjectAttribute.new(:to_one, singular_name, self)
	attrb.classes = isas
	add_attribute(attrb)

   # 2. 添加名为 `attrb.name` 的 Getter 方法
	attr_reader(attrb.name)
	# 1.9.2 fix, see https://github.com/CocoaPods/Xcodeproj/issues/40.
	public(attrb.name)

   # 3. 添加名为 `attrb.name=` 的 Setter 方法
	variable_name = :"@#{attrb.name}"
	define_method("#{attrb.name}=") do |value|
		attrb.validate_value(value)

		previous_value = send(attrb.name)
		return value if previous_value == value
		mark_project_as_dirty!
		previous_value.remove_referrer(self) if previous_value
		instance_variable_set(variable_name, value)
		value.add_referrer(self) if value
	end
end

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

  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 为数组。

isas = [isas] unless isas.is_a?(Array)

而对于非 Array 类型则会手动创建 Array 来封装。Why ?为了解决 project.pbxproj 中一个属性可以支持多种不同 isa 类型。举个例子,如 PBXBuildFile 的 file_ref:

has_one :file_ref, [
	PBXFileReference,
	PBXGroup,
	PBXVariantGroup,
	XCVersionGroup,
	PBXReferenceProxy,
]

而对于仅支持一种 isa 类型的属性,如:

has_one :build_configuration_list, XCConfigurationList

像这样声明的 build_configuration_list 属性其实是个数组:[XCConfigurationList]

容器类引用

has_many

has_many 用于定义一组有序的引用关系。它仅生成了 reader 方法,这里不提供 setter 方法是由于关联属性是记录在数组中,而声明创建 ObjectList 对象仅是作为容器,因此无需提供 setter。

def has_many(plural_name, isas)
   isas = [isas] unless isas.is_a?(Array)
   # 1. 创建 AbstractObjectAttribute 
	attrb = AbstractObjectAttribute.new(:to_many, plural_name, self)
	attrb.classes = isas
	add_attribute(attrb)

   # 2. 添加名为 `attrb.name` 的 Getter 方法
	variable_name = :"@#{attrb.name}"
	define_method(attrb.name) do
		# Here we are in the context of the instance
		list = instance_variable_get(variable_name)
		unless list
			list = ObjectList.new(attrb, self)
			instance_variable_set(variable_name, list)
		end
		list
	end
end

第一步也是创建并保存 AbstractObjectAttribute。getter 则比较简单,以惰性初始化的方式返回 ObjectList 数组。为了管理对象的引用计数,作为 Array 的子类 ObjectList 也提供了 add_referrerremove_referrer 来更新引用计数,后续会展开。

has_many 使用如下:

class PBXNativeTarget < AbstractTarget
	# @return [PBXBuildRule] the build rules of this target.
	has_many :build_rules, PBXBuildRule
end

class PBXGroup < AbstractTarget
	# @return [PBXBuildRule] the build phases of the target.
	has_many :children, [PBXGroup, PBXFileReference, PBXReferenceProxy]
end

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 来记录该引用关系:

/* Begin PBXProject section */
		85BB2A4F26DA8CB600AE6943 /* Project object */ = {
			isa = PBXProject;
			mainGroup = 85BB2A4E26DA8CB600AE6943;
			...
			projectReferences = (
				{
					ProductGroup = 85BB2A9126DA916100AE6943 /* Products */;
					ProjectRef = 85BB2A9026DA916100AE6943 /* A.xcodeproj */;
				},
			);
		};
/* End PBXProject section */

另外需要注意的是,由于引用的是一个完整的 Project,这里的 ProjectRef 指向的是一个 PBXFileReference

85BB2A9026DA916100AE6943 /* A.xcodeproj */ = {
	isa = PBXFileReference;
	lastKnownFileType = "wrapper.pb-project";
	name = A.xcodeproj;
	path = A/A.xcodeproj;
	sourceTree = "<group>";
};

这也给我们一个示例,大型工程的功能代码该如何高效组织。而 CocoaPods 在 1.7 版本中提供的 install feature:generate_multiple_pod_projects 则是他们提供的解决方案。

从历史上看,CocoaPods 总是生成一个 Pods.xcodeproj,其中包含项目编译所需的所有目标和构建设置。对于较小的项目,仅使用一个包含整个 Podfile 的项目就可以了;但是,随着项目的增长,Pods.xcodeproj 文件的大小也会增加。

Pods.xcodeproj 文件越大,Xcode 解析其内容所需的时间就越长,这会导致 Xcode 体验下降。通过将每个 pod 集成为自己独立的 Xcode 项目并嵌套在顶级 Pods.xcodeproj 下,从而为更大的 CocoaPods 项目带来了一些显着的性能改进。

此外,在大型代码库中,此功能可能特别有用,因为开发人员可以选择仅打开他们需要处理的特定 .xcodeproj(位于 Pods/ 目录下),而不是打开整个工作区,这会减慢他们的开发过程。

回到本文中,has_many_references_by_key 就是用于记录 projectReferences,其实现如下:

def has_many_references_by_keys(plural_name, classes_by_key)
	attrb = AbstractObjectAttribute.new(:references_by_keys, plural_name, self)
	attrb.classes = classes_by_key.values
	attrb.classes_by_key = classes_by_key
	add_attribute(attrb)

	# Getter 实现同 has_many, 通过 ObjectList 来存储
   ...
end

它是作为 PBXProject 属性来关联其他项目文件,声明如下:

class PBXProject < AbstractObject
   # @return [Array<ObjectDictionary>] any reference to other projects.
	has_many_references_by_keys :project_references,
                               :project_ref   => PBXFileReference,
                               :product_group => PBXGroup
end

可以注意到,这里的存储的元素为 ObjectDictionary,所记录的键值对为:

{
	:project_ref   => PBXFileReference,
	:product_group => PBXGroup
}

ObjectList 类似,ObjectDictionary 作为 Hash 的子类也提供了 add_referrerremove_referrer 来更新引用计数。具体我们在下一节展开。

Object Configuration

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

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

可以看到方法 Object::objects_by_uuid 会调用 Project::new_from_plist 形成递归,直至 objects 解析完成。

configure_with_plist

让我们将 Object::configure_with_plist 展开,内容主要分 5 个部分:

def configure_with_plist(objects_by_uuid_plist)
   #1. 根据 uuid 获取 object 的属性字典
	object_plist = objects_by_uuid_plist[uuid].dup

	unless object_plist['isa'] == isa
		raise "[Xcodeproj] Attempt to initialize `#{isa}` from plist with " \
			"different isa `#{object_plist}`"
	end
	object_plist.delete('isa')
   #2. 常规属性解析
	simple_attributes.each do |attrb|
		attrb.set_value(self, object_plist[attrb.plist_name])
		object_plist.delete(attrb.plist_name)
	end
   #3. 直接引用 Object 属性解析
	to_one_attributes.each do |attrb|
		ref_uuid = object_plist[attrb.plist_name]
		if ref_uuid
			ref = object_with_uuid(ref_uuid, objects_by_uuid_plist, attrb)
			attrb.set_value(self, ref) if ref
		end
		object_plist.delete(attrb.plist_name)
	end
   #4. 数组容器引用 Object 属性解析
	to_many_attributes.each do |attrb|
		ref_uuids = object_plist[attrb.plist_name] || []
		list = attrb.get_value(self)
		ref_uuids.each do |uuid|
			ref = object_with_uuid(uuid, objects_by_uuid_plist, attrb)
			list << ref if ref
		end
		object_plist.delete(attrb.plist_name)
	end
   #5. 数组容器引用 Project 属性解析
	references_by_keys_attributes.each do |attrb|
		hashes = object_plist[attrb.plist_name] || {}
		list = attrb.get_value(self)
		hashes.each do |hash|
			dictionary = ObjectDictionary.new(attrb, self)
			hash.each do |key, uuid|
				ref = object_with_uuid(uuid, objects_by_uuid_plist, attrb)
				dictionary[key] = ref if ref
			end
			list << dictionary
		end
		object_plist.delete(attrb.plist_name)
	end

	unless object_plist.empty?
		UI.warn "[!] Xcodeproj doesn't know about the following " \
						"attributes #{object_plist.inspect} for the '#{isa}' isa." \
						"\nIf this attribute was generated by Xcode please file " \
						'an issue: https://github.com/CocoaPods/Xcodeproj/issues/new'
	end
end
  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。该方法实现如下:

def object_with_uuid(uuid, objects_by_uuid_plist, attribute)
	unless object = project.objects_by_uuid[uuid] || project.new_from_plist(uuid, objects_by_uuid_plist)
		UI.warn "`#{inspect}` attempted to initialize an object with an unknown UUID. "
		...
	end
	object
rescue NameError
	attributes = objects_by_uuid_plist[uuid]
	raise "`#{isa}` attempted to initialize an object with unknown ISA "
	...
end

首先从 project 的 objects 表 objects_by_uuid 中查询 object,如不存在,则会通过 Project::new_from_plist 解析出对应的 object 并存入 objects 表中。

def new_from_plist(uuid, objects_by_uuid_plist, root_object = false)
  attributes = objects_by_uuid_plist[uuid]
  if attributes
    #1. 
    klass = Object.const_get(attributes['isa'])
    object = klass.new(self, uuid)
	 #2. 
    objects_by_uuid[uuid] = object
    object.add_referrer(self) if root_object
    #3. 
    object.configure_with_plist(objects_by_uuid_plist)
    object
  end
end
  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_referrer + remove_referrer。而该方法除了 has_one 属性会修改之外,ObjectListObjectDictionary 容器均提供了对应的接口。

add_referrer

class AbstractObject
	attr_reader :referrers
   
	def add_referrer(referrer)
		@referrers << referrer
		@project.objects_by_uuid[uuid] = self
	end

	def remove_referrer(referrer)
		@referrers.delete(referrer)
		if @referrers.count == 0
			mark_project_as_dirty!
			@project.objects_by_uuid.delete(uuid)
		end
	end
end

下面我们重点聊 add_referrer,删除引用的逻辑同添加类似就不展开了。add_referrer 实现就两行,比较简单。首先会通知 object 另一个对象正在引用它,并将其记录到 referrers 中。如果 object 之前没有引用,则将其添加到 objects_by_uuid 表中。

前面提到 Xcodeproj 支持编辑 project.pbxproj,而对 project.pbxproj 的编辑本质上是修改 object 属性与 objects_by_uuid。因此,我们要做的事情就是保证引用属性被修改时,对应的 referres 和 objects_by_uuid 的同步。同步逻辑就封装在 Xcodeproj 的引用修饰器中:has_onehas_manyhas_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 中。

def new_file_reference(group, path, source_tree)
	path = Pathname.new(path)
	ref = group.project.new(PBXFileReference)
	group.children << ref
	GroupableHelper.set_path_with_source_tree(ref, path, source_tree)
	ref.set_last_known_file_type
	ref
end

PBXGroupchildren 属性为例,它就是通过 ObjectList<< 方法来添加 PBXFileReference

def <<(object) ... end
def +(object) ... end
def insert(index, object) ... end
def unshift(object) ... end

这几个增加 object 方法,均通过 perform_additions_operations 方法完成 object 的 add_referrer 方法调用。

def perform_additions_operations(objects)
	objects = [objects] unless objects.is_a?(Array)
	objects.each do |obj|
		owner.mark_project_as_dirty!
		obj.add_referrer(owner)
		attribute.validate_value(obj) unless obj.is_a?(ObjectDictionary)
	end
end

has_many_references_by_keys

has_many_references_by_keys 不同于 has_many 的是其储存的元素为 ObjectDictionary,它通过重载 Hash 的更新方法来更新引用:

def []=(key, object)
	key = normalize_key(key)
	if object
		perform_additions_operations(object, key)
	else
		perform_deletion_operations(self[key])
	end
	super(key, object)
end

def perform_additions_operations(object, key)
	owner.mark_project_as_dirty!
	object.add_referrer(owner)
	attribute.validate_value_for_key(object, key)
end

perform_additions_operations 中,同样调用 object 的 add_referrer 完成引用更新。

通过 CLI 添加 Xcode 子项目依赖

最后一节,我们通过命令行添加 Xcode 子项目,来实践一下前面提到的知识点。

在前文提到的 new_reference 方法中,当新建引用文件类型为 .xcodeproj 时,会调用 new_subproject,该方法就是用于添加 Xcode 子项目的引用。

def new_reference(group, path, source_tree)
  ref = case File.extname(path).downcase
        when '.xcdatamodeld'
          new_xcdatamodeld(group, path, source_tree)
        when '.xcodeproj'
          new_subproject(group, path, source_tree)
        else
          new_file_reference(group, path, source_tree)
        end

  configure_defaults_for_file_reference(ref)
  ref
end

def new_subproject(group, path, source_tree)
   #1. 新建 sub project 文件引用
	ref = new_file_reference(group, path, source_tree)
	ref.include_in_index = nil
   
   #2.1 新建 sub product groups 分组
	product_group_ref = find_products_group_ref(group, true)

   #2.2 遍历子项目的 objects 表,将其 product framework 作为当前项目的的产物代理
	subproj = Project.open(path)
	subproj.products_group.files.each do |product_reference|
		container_proxy = group.project.new(PBXContainerItemProxy)
		container_proxy.container_portal = ref.uuid
		container_proxy.proxy_type = Constants::PROXY_TYPES[:reference]
		container_proxy.remote_global_id_string = product_reference.uuid
		container_proxy.remote_info = 'Subproject'

		reference_proxy = group.project.new(PBXReferenceProxy)
		extension = File.extname(product_reference.path)[1..-1]
		reference_proxy.file_type = Constants::FILE_TYPES_BY_EXTENSION[extension]
		reference_proxy.path = product_reference.path
		reference_proxy.remote_ref = container_proxy
		reference_proxy.source_tree = 'BUILT_PRODUCTS_DIR'

		product_group_ref << reference_proxy
	end
	#4. 创建 ObjectDictionary,将 subproject 的文件引用和产物引用记录其中,最后将 ObjectDictionary 存入 project_references
	attribute = PBXProject.references_by_keys_attributes.find { |attrb| attrb.name == :project_references }
	project_reference = ObjectDictionary.new(attribute, group.project.root_object)
	project_reference[:project_ref] = ref
	project_reference[:product_group] = product_group_ref
	group.project.root_object.project_references << project_reference

	ref
end

通过前面介绍的 Xcode 子项目依赖可知,添加子项目后 Xcode 工程文件中多出了字典 projectReferences,它包含两个 key project_refproduct_group。想要以 CLI 的方式添加 Xcode 子项目依赖,就需要构造出 projectReferences 字典。

具体步骤说明如下:

0x01 新建子工程文件引用

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

0x02 新建子工程 Product 引用

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

完整引用关系如下:

  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 子项目?

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK