9. Xcode 工程文件编辑
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.
通过「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
对象就同时保存了对 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 方法来维护引用关系。
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
从这里的逻辑可以分为三部分,同常规属性修饰器实现一致:
- 创建
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 为数组。
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_referrer
与 remove_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_referrer
与 remove_referrer
来更新引用计数。具体我们在下一节展开。
Object Configuration
前文整体介绍了项目文件 project.pbxproj
的解析。而对于核心方法 Object::configure_with_plist 并未展开,因为它涉及到本文的两个重要类型 ObjectList
与 ObjectDictionary
,为了方便理解,我们在这里来详细分析。
首先,回顾一下 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
- 根据 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。该方法实现如下:
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
- 以 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
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_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
中。
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
以 PBXGroup
的 children
属性为例,它就是通过 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_ref
与 product_group
。想要以 CLI 的方式添加 Xcode 子项目依赖,就需要构造出 projectReferences
字典。
具体步骤说明如下:
0x01 新建子工程文件引用
通过 new_file_reference 创建 PBXReference
以指向子项目的工程文件地址。
0x02 新建子工程 Product 引用
当我们添加 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 子项目?
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK