10-xcode-project-src

引子

在「Molinillo 依赖校验」通过后,CocoaPods 会根据确定的 PodSpec 下载对应的源代码和资源,并为每个 PodSpec 生成对应的 Xcode Target。本文重点就来聊聊 Xcode Project 的内容构成,以及 xcodeproj 是如果组织 Xcode Project 内容的。

Xcode 工程文件

早在前文「Podfile 的解析逻辑」中,我们简单介绍过 Xcode 的工程结构:Workspace、Project、Target 及 Build Setting 等。

04-workspace

我们先来了解下这些数据在 Xcode 中是如何表示,了解这些结构才能方便我们理解 xcodeproj 的代码设计思路。

xcworkspace

早在 Xcode 4 之前就出现 workspace bundle 了,只是那会 workspace 仍内嵌于 .xcodeproj 中。Xcode 4 之后,我们才对 workspace 单独可见。让我们新建一个 Test.xcodeproj 项目,来看看其目录结构:

 1Test.xcodeproj
 2├── project.pbxproj
 3├── project.xcworkspace
 4│   ├── contents.xcworkspacedata
 5│   └── xcuserdata
 6│       └── edmond.xcuserdatad
 7│           └── UserInterfaceState.xcuserstate
 8└── xcuserdata
 9    └── edmond.xcuserdatad
10        └── xcschemes
11            └── xcschememanagement.plist

可以发现 Test.xcodeproj bundle 内包含 project.workspace。而当我们通过 pod install 命令成功添加 Pod 依赖后,Xcode 工程目录下会多出 Test.workspace,它是 Xcodeproj 替我们生成的,用于管理当前的 Test.projectPods.pbxproj。新建的 workspace 目录如下:

1Test.xcworkspace
2└── contents.xcworkspacedata

生成的 workspace 文件夹内部只包含了 contents.xcworkspacedata,为 xml 格式的内容:

 1<?xml version="1.0" encoding="UTF-8"?>
 2<Workspace
 3   version = "1.0">
 4   <FileRef
 5      location = "group:Test.xcodeproj">
 6   </FileRef>
 7   <FileRef
 8      location = "group:Pods/Pods.xcodeproj">
 9   </FileRef>
10</Workspace>

在标签 Workspace 下声明了两个 FileRef 其地址分别指向了 Test.xcodeprojPods.xcodeproj。这里注意的是 FileRef 属性的值使用前缀 group + path 来修饰的,而内嵌的 project.xcworkspace,使用 self 作为前缀:

1<?xml version="1.0" encoding="UTF-8"?>
2<Workspace
3   version = "1.0">
4   <FileRef
5      location = "self:">
6   </FileRef>
7</Workspace>

另外,当我们用 Xcode 打开项目后,能发现 workspace 目录下会自动生成 xcuserdata 目录,它用于保存用户的 Xcode 配置。比如:

  • UserInterfaceState.xcuserstate:以二进制的 Plist 保存,用于记录窗口布局等个性化设置。
  • xcdebugger:记录各种断点数据。

这也是在日常开发中,经常会选择将 xcuserdata 目录 ignore 掉的原因。

xcodeproj

.xcworkspace 类似 .xcodeproj 同为 Xcode 工程配置的 bundle,接下来重点展开 project.pbxproj,它记录了 Xcode 工程的各项配置,本质上是一种旧风格的 Plist 文件。

Property List

Plist 被设计为人类可读的、可以手工修改的格式,故采用了类似于编程语言的语法将数据序列化为ASCII数据。Plist 最早可追溯到 NeXTSTEP 时期,由于历史原因,目前它支持多种格式,string、binary、array、dictionary 等类型数据。相比于 JSON,Plist 还支持二进制数据的表示,以 <> 修饰文本形式的十六进制数,其中字典与数组的区别如下:

1Array:
2plist => ( "1", "2", "3" )
3json => [ "1", "2", "3" ]
4
5Dictionary:
6plist => { "key" = "value"; ... }
7json => { "key" : "value", ... }

处理 Plist 文件可使用 Unix 提供的 plutil 工具。 比如将 Plist 文件转成 XML 格式:

1plutil -convert xml1 -s -r -o project.pbxproj.xml project.pbxproj

-convert fmt 选项支持转换的格式有:xml1、binary1、json、swift、json。

project.pbxproj

pbxproj 文件全称为 Project Builder Xcode Project,光看第一层元数据比较简单:

 1// !$*UTF8*$!
 2{
 3	archiveVersion = 1;
 4	classes = {
 5	};
 6	objectVersion = 50;
 7	objects = {
 8		...
 9   };
10	rootObject = 8528A0D92651F281005BEBA5 /* Project object */;
11}

文件以明确的编码信息开头,archiveVersion 通常为 1,表示序列化的版本号;classes 则似乎一直为空;objectVersion 表示所兼容的最低版本的 Xcode,该数字与 Xcode 的版本对应关系如下:

153 => 'Xcode 11.4',
252 => 'Xcode 11.0',
351 => 'Xcode 10.0',
450 => 'Xcode 9.3',
548 => 'Xcode 8.0',
647 => 'Xcode 6.3',
746 => 'Xcode 3.2',
845 => 'Xcode 3.1',

rootObject 记录的 16 进制数字,为 project 对象的索引。这里我们可以称其为 Xcode Object Identifier,pbxproj 中的每个 Xcode Object 创建时,都会生成对应唯一标识数字,而上面的 objects 字典则记录了整个 Xcode 项目的所有 Xcode Object。

Xcode Object Identifiers

Xcode Object Identifier 是用 24 位的 16 进制字符表示,我们暂且称其为 GUID。

⚠️ 注意这并不意味着它与其他称为 GUID 的其他事物相似。

生成的 GUID 不仅在项目文件中必须唯一,并且在 Xcode 中同时打开的其他项目文件中也必须唯一,即跨工程唯一性。只有这样能避免了多人合作中,同时新增或编辑工程文件带来的问题。这其实是一个有趣的竞争需求,有兴趣的可以查看 Premake 这个项目,它能保证重新生成的项目具有相同的 GUID。

对于 Xcode 使用的 GUID 算法可参考 PBXProj Identifers,它是通过逆向 DevToolsCore.framework 获取的,从中可以窥见 GUID 的生成需要依赖 👇 这些数据:

 1struct globalidentifier {
 2    int8_t user;            // encoded username
 3    int8_t pid;             // encoded current pid #
 4    int16_t _random;        // encoded random value
 5    int32_t _time;          // encoded time value
 6    int8_t zero;            // zero
 7    int8_t host_shift;      // encoded, shifted hostid
 8    int8_t host_h;          // high byte of lower bytes of hostid
 9    int8_t host_l;          // low byte of lower bytes of hostid
10} __attribute__((packed));  // packed, so we always have a length of 12 bytes

Xcode Object

Xcode Object 是指所有记录在 objects 字典中的对象 (后续简称为 Object),内部以 isa 来标识类型。同时Xcode 按 isa 类型划分出若干 section,以注释的方式分节。

PBXProject 为例:

 1/* Begin PBXProject section */
 2   8528A0D92651F281005BEBA5 /* Project object */ = {
 3      isa = PBXProject;
 4      ...
 5      mainGroup = 8528A0D82651F281005BEBA5;
 6      productRefGroup = 8528A0E22651F281005BEBA5 /* Products */;
 7      projectDirPath = "";
 8      projectRoot = "";
 9      targets = ( 
10         8528A0E02651F281005BEBA5 /* Test */,
11      );
12   };
13/* End PBXProject section */

这里 Project object 的索引值正是 rootObject 记录的 8528A0D92651F281005BEBA5,其 isa 类型为 PBXProjecttargets 数组记录了项目需要构建的任务:Test target。整个 PBXProject section 就定义在 objects 中。

Object 类型的引用关系大致如下图:

00-xcodte-project

关于 pbxproj 内 Object 的详细说明,可参照 Xcode Project File Format

  • PBXProject:Project 的设置,编译工程所需信息
  • PBXNativeTarget:Target 的设置
  • PBXTargetDependency: Target 依赖
  • PBXContainerItemProxy:部署的元素
  • XCConfigurationList:构建配置相关,包含 project 和 target 文件
  • XCBuildConfiguration:编译配置,对应 Xcode 的 Build Setting 内容
  • PBXVariantGroup:国际化对照表或 .storyboard 文件
  • PBXBuildFile:各类文件,最终会关联到 PBXFileReference
  • PBXFileReference:源码、资源、库,Info.plist 等文件索引
  • PBXGroup:虚拟文件夹,可嵌套,记录管理的 PBXFileReference 与子 PBXGroup
  • PBXSourcesBuildPhase:编译源文件(.m、.swift)
  • PBXFrameworksBuildPhase:用于 framework 的构建
  • PBXResourcesBuildPhase:编译资源文件,有 xib、storyboard、plist 以及 xcassets 等资源文件

Xcodeproj

Xcodeproj 能够通过 Ruby 来创建和修改 Xcode 工程文件,其内部完整映射了 project.pbxproj 的 Object 类型及其对应的属性,限于篇幅本文会重点介绍关键的 Object 和解析逻辑。

Project 解析

上节内容可知,Xcode 解析工程的是依次检查 *.xcworkspace > *.xcproject > project.pbxproj,根据 project.pbxproj 的数据结构,Xcodeproj 提供了 Project 类,用于记录根元素。

 1module Xcodeproj
 2  class Project
 3    # object 模块,后面会提到
 4    include Object	
 5    ...
 6    # 序列化时 project 的最低兼容版本, 与 object_version 对应
 7    attr_reader :archive_version
 8    # 作用未知
 9    attr_reader :classes
10    # project 最低兼容版本
11    attr_reader :object_version
12    # project 所包含的 objects,结构为 [Hash{String => AbstractObject}]
13    attr_reader :objects_by_uuid
14    # project 的根结点,即 PBXProject
15    attr_reader :root_object
16  end
17end

pod install 的依赖解析阶段,会读取 project.pbxproj

01-resolve-dependencies

最终在 inspect_targets_to_integrate 方法内打开项目:

1def inspect_targets_to_integrate
2  project = Xcodeproj::Project.open(project_path)
3  ...
4end

继续看 Xcodeproj::Project::open 实现:

 1# lib/xcproject/Project.rb
 2
 3def self.open(path)
 4  path = Pathname.pwd + path
 5  unless Pathname.new(path).exist?
 6    raise "[Xcodeproj] Unable to open `#{path}` because it doesn't exist."
 7  end
 8  project = new(path, true)
 9  project.send(:initialize_from_file)
10  project
11end
12
13def initialize_from_file
14  pbxproj_path = path + 'project.pbxproj'
15  plist = Plist.read_from_path(pbxproj_path.to_s)
16  root_object.remove_referrer(self) if root_object
17  @root_object     = new_from_plist(plist['rootObject'], plist['objects'], self)
18  @archive_version = plist['archiveVersion']
19  @object_version  = plist['objectVersion']
20  @classes         = plist['classes'] || {}
21  @dirty           = false
22
23  unless root_object
24    raise "[Xcodeproj] Unable to find a root object in #{pbxproj_path}."
25  end
26
27  if archive_version.to_i > Constants::LAST_KNOWN_ARCHIVE_VERSION
28    raise '[Xcodeproj] Unknown archive version.'
29  end
30
31  if object_version.to_i > Constants::LAST_KNOWN_OBJECT_VERSION
32    raise '[Xcodeproj] Unknown object version.'
33  end
34
35  # Projects can have product_ref_groups that are not listed in the main_groups["Products"]
36  root_object.product_ref_group ||= root_object.main_group['Products'] || root_object.main_group.new_group('Products')
37end
  1. 在 open 方法中会检验路径,初始化 Project 对象;
  2. 使用内部 Property List 类 Plist 来读取 project.pbxproj 数据;

需要注意的是 new_from_plist 不仅完成了 rootObject 的解析,同时也完成 objects 的解析。

 1def new_from_plist(uuid, objects_by_uuid_plist, root_object = false)
 2  attributes = objects_by_uuid_plist[uuid]
 3  if attributes
 4    # 以 isa 获取 Xcodeproj 中对应的 Xcode Object 类型
 5    klass = Object.const_get(attributes['isa'])
 6    object = klass.new(self, uuid)
 7    objects_by_uuid[uuid] = object
 8    object.add_referrer(self) if root_object
 9    # 解析 objects 节点,并完成 ISA 数据到 Xcode Object 的映射
10    object.configure_with_plist(objects_by_uuid_plist)
11    object
12  end
13end
  1. 以 uuid 取出 Plist 数据并利用 isa kclass 完成 Project 对象的初始化和映射。Object const 中记录了支持的 isa
  2. 将 object 存入 objects_by_uuid 字典中,以覆盖原有的 GUID;
  3. 执行 configure_with_plist 递归,完成 objects 映射。

Tips: configure_with_plist 于下篇展开.

project.pbxproj 解析主体流程如下

02-parse-project

图中的 reference attributes 是指 Object 的引用属性,后面会有介绍。

Object Module

AbstractObject

Object 的抽象类,Xcode 项目并不存在对应的 isa 类型。先简单介绍部分属性,方便后续理解。

 1class AbstractObject
 2  # object 类型
 3  attr_reader :isa
 4  # object 唯一标识
 5  attr_reader :uuid
 6  # 持有 object 的 project
 7  attr_reader :project
 8
 9  def initialize(project, uuid)
10    @project = project
11    @uuid = uuid
12    @isa = self.class.isa
13    @referrers = []
14    unless @isa =~ /^(PBX|XC)/
15      raise "[Xcodeproj] Attempt to initialize an abstract class (#{self.class})."
16    end
17  end
18  ...
19end

注意,Object 类在 initialize 时关联了 project 对象,作者不建议我们直接使用,而在 Xcodeproj 模块内提供了 convince 方法。

1def new(klass)
2   if klass.is_a?(String)
3     klass = Object.const_get(klass)
4   end
5   object = klass.new(self, generate_uuid)
6   object.initialize_defaults
7   object
8end

作者保留 project 的引用是为了方便处理 Object 的引用计数,project 作为项目的根节点,记录了完整的工程配置和各模块的交叉引用关系,有了 project 的引用能给进行 object 的引用管理。

Const Accessor

这里先插播一下 Object 模块中 const 定义及其存储的值。在 Ruby 中 Module 可以在运行时添加模块级的常量,方法如下:

1Module#const_get
2Module#const_set

那么问题来了,这些 const 常量是在什么时机存入 Object 中的呢 ?

1# lib/xcodeproj/project/object.rb
2
3Xcodeproj::Constants::KNOWN_ISAS.each do |superclass_name, isas|
4  superklass = Xcodeproj::Project::Object.const_get(superclass_name)
5  isas.each do |isa|
6    c = Class.new(superklass)
7    Xcodeproj::Project::Object.const_set(isa, c)
8  end
9end

在 object.rb 文件中,可以定位到 Object.const_set 的调用,它是在 Object 类被加载时触发的。Xcodeproj 支持的 isa 类型名称都定义在 Xcodeproj::Constants::KNOWN_ISAS 中。Object 加载时通过遍历它来载入 isa class

 1KNOWN_ISAS = {
 2  'AbstractObject' => %w(
 3    PBXBuildFile
 4    AbstractBuildPhase
 5    PBXBuildRule
 6    XCBuildConfiguration
 7    XCConfigurationList
 8    PBXContainerItemProxy
 9    PBXFileReference
10    PBXGroup
11    PBXProject
12    PBXTargetDependency
13    PBXReferenceProxy
14    AbstractTarget
15  ),
16
17  'AbstractBuildPhase' => %w(
18    PBXCopyFilesBuildPhase
19    PBXResourcesBuildPhase
20    PBXSourcesBuildPhase
21    PBXFrameworksBuildPhase
22    PBXHeadersBuildPhase
23    PBXShellScriptBuildPhase
24  ),
25
26  'AbstractTarget' => %w(
27    PBXNativeTarget
28    PBXAggregateTarget
29    PBXLegacyTarget
30  ),
31
32  'PBXGroup' => %w(
33    XCVersionGroup
34    PBXVariantGroup
35  ),
36}.freeze

Object Attributes

AbstractObject 作为 Xcodeproj 提供的 Object 基类,它不仅提供了基本属性,如 isa 类型和 uuid 等,还提供了特殊的属性修饰器如 Attribute,方便快速添加属性。光从 KNOWN_ISAS 看来这些 isa 类型就已经够多了,还要考虑属性的 CURD 以及数据到对象映射等一系列操作。

为此,Xcodeproj 实现了一套针对 project.pbxproj 的 DSL 解析。这些特性的实现正是依赖于属性修饰器和 Ruby 强大的 Runtime 能力。

AbstractObjectAttribute

AbstractObjectAttribute 是用于声明和存储 Object 属性的关键信息,定义如下:

 1class AbstractObjectAttribute
 2   
 3  # 属性本身类型,:simple, :to_one, :to_many.
 4  attr_reader :type
 5   
 6  # 属性名
 7  attr_reader :name
 8   
 9  # 属性的持有者
10  attr_accessor :owner
11
12  def initialize(type, name, owner)
13    @type  = type
14    @name  = name
15    @owner = owner
16  end
17
18  # 属性支持的类型
19  attr_accessor :classes
20   
21  # 仅限于 :references_by_keys 关联的类型
22  attr_accessor :classes_by_key
23   
24  # 仅限于 :simple 类型指定默认值
25  attr_accessor :default_value
26  ...
27end

AbstractObject 通过 Ruby 提供的 Singleton Classes 以实现属性修饰器方法的扩展。

对于单例模式,Ruby 提供了多种实现方式。在 Ruby 中,每个对象可以拥有一个匿名单例类。默认情况下,自定义类是不存在单例类,不过可通过 class << obj 来打开对象的单例类空间。

1class C
2  class << self
3    puts self	# 输出:#<Class:C>,类 C 类对象的单例类
4  end
5end

注意:Ruby 中每个对象 (Class / Module 也是对象) 都有自己所属的类,它们都是有具体名称的类。

Xcodeproj 针对 Attribute 提供了多种单例类方法,定义如下:

 1module Object
 2  class AbstractObject
 3    class << self
 4      # 普通属性声明方法
 5      def attribute(name, klass, default_value = nil) ... end
 6       
 7      # 单一引用的属性声明方法
 8      def has_one(singular_name, isas) ... end
 9       
10      # 多引用的属性声明方法
11      def has_many(plural_name, isas) ... end
12       
13      # 多引用且不同 key 的属性声明方法
14      def has_many_references_by_keys(plural_name, classes_by_key) ... end
15    end
16  end
17end

直接看如何使用,声明属性比较直观:

 1module Object
 2  class PBXProject < AbstractObject     
 3     
 4    # 声明为 [String] 类型的 project_dir_path,默认值为空
 5    attribute :project_dir_path, String, ''
 6    
 7    # 声明为 [PBXGroup] 类型的 main_group
 8    has_one :main_group, PBXGroup
 9
10    # 声明为 [ObjectList<AbstractTarget>] 类型的 targets
11    has_many :targets, AbstractTarget
12     
13    # 声明为 [Array<ObjectDictionary>] 类型的 project_references 
14    has_many_references_by_keys :project_references,
15                                :project_ref   => PBXFileReference,
16                                :product_group => PBXGroup
17    ...
18  end
19end

所谓的单一或者多引用是指Object 之间引用关系。以 PBXProject 为例,一个项目只能指定一个 mainGroup,但对于 targets 可以存在多个。这里不直接使用 attribute 来修饰是因为,像 target 这种存在交叉引用的 Object 被删除时,我们需要知道谁引用了它,才保证所有的引用都能被清理干净。因此,Xcodeproj 实现了一套针对 Object 的引用计数管理 (不在本文讨论范围)。

Attribute

今天先看普通属性的 DSL 实现,has_onehas_many 的属性修饰将在下篇展开。

 1# lib/xcodeproj/project/object_attributes.rb
 2
 3def attribute(name, klass, default_value = nil)
 4  # 1. 初始化 attribute,记录属性名,属性类型及默认值
 5  attrb = AbstractObjectAttribute.new(:simple, name, self)
 6  attrb.classes = [klass]
 7  attrb.default_value = default_value
 8  # 2. 将 attrb 存入 @attributes
 9  add_attribute(attrb)
10  
11  # 3. 添加名为 name 的 getter
12  define_method(attrb.name) do
13    @simple_attributes_hash ||= {}
14    @simple_attributes_hash[attrb.plist_name]
15  end
16
17  # 4. 添加名为 #{name}= 的 setter,并将值存入 @simple_attributes_hash 字典中
18  define_method("#{attrb.name}=") do |value|
19    @simple_attributes_hash ||= {}
20    # 5. 检查 value 是否为 klass 支持的类型
21    attrb.validate_value(value)
22
23    existing = @simple_attributes_hash[attrb.plist_name]
24    if existing.is_a?(Hash) && value.is_a?(Hash)
25      return value if existing.keys == value.keys && existing == value
26    elsif existing == value
27      return value
28    end
29    mark_project_as_dirty!
30    @simple_attributes_hash[attrb.plist_name] = value
31  end
32end

代码比较长核心逻辑就 2 步:

  1. 初始化 attribute,记录属性名,属性类型及默认值,将 attrb 存入对象的 @attributes 用于后续遍历;

  2. 利用 define_method 为修饰的对象添加实例变量存取方法。

    由于修饰的属性类型不同,accessor 方法直接使用 Hash 容器来保存变量值,key 为 attribute.name,使用 Hash 是为了避免受到 Object 引用计数的影响。

本质上 attribute 为我们生成的 accessor 如下:

1def project_dir_path
2  @simple_attributes_hash[projectDirPath]
3end
4
5def project_dir_path=(value)
6  attribute.validate_value(value)
7  @simple_attributes_hash[projectDirPath] = value
8end

Xcode Object

最后我们来聊两个 project.pbxproj 中的基础 Object 类型:PBXFileReferencePBXGroup。放在文章末尾,是希望看到这里的同学能理解Xcode 设计的背后思想。

PBXFileReference

PBXFileReference 记录了构建 Xcode 项目所需要的真实文件信息,主要记录了文件路径。这些 PBXFileReference 就是我们在 Xcode 项目的左侧边栏中所见的文件。

1F8DA09E31396AC040057D0CC /* main.m */ = {
2  isa = PBXFileReference; 
3  fileEncoding = 4;
4  lastKnownFileType = sourcecode.c.objc; 
5  path = main.m; 
6  sourceTree = SOURCE_ROOT; 
7};

对于 PBXFileReference 路径,重点在于确认该 path 是相对路径还是绝对路径,sourceTree 就是用来描述这个的,它有几种相对关系:

  • <absolute>:绝对路径

  • <group> 基于 PBXGroup 的相对路径

  • SOURCE_ROOT 基于 project 的相对路径

  • DEVELOPER_DIR 基于 developer directory 的相对路径

  • BUILT_PRODUCTS_DIR 基于 build products directory 的相对路径

  • SDKROOT 基于 SDK directory 的相对路径

Xcodeproj 提供了 GroupableHelper 用于获取各种状态的文件目录,比如文件的绝对目录:

 1def source_tree_real_path(object)
 2  case object.source_tree
 3  when '<group>'
 4    object_parent = parent(object)
 5    if object_parent.isa == 'PBXProject'.freeze
 6      object.project.project_dir + object.project.root_object.project_dir_path
 7    else
 8      real_path(object_parent)
 9    end
10  when 'SOURCE_ROOT'
11    object.project.project_dir
12  when '<absolute>'
13    nil
14  else
15    Pathname.new("${#{object.source_tree}}")
16  end
17end

PBXFileReference 主要定义如下:

1module Object
2  class PBXFileReference < AbstractObject
3    attribute :name, String
4    attribute :path, String
5    attribute :source_tree, String, 'SOURCE_ROOT'
6end

PBXGroup

基于 PBXFileReference 之上的一层抽象,能更好的对 PBXFileReference 进行管理和分组。PBXGroup 背后并不一定真实存在对应的文件夹,另外 PBXGroup 可以引用其他 PBXGroup,进行嵌套。定义如下:

 1module Object
 2  class PBXGroup < AbstractObject
 3    # group 可包含的类型
 4    has_many :children, [PBXGroup, PBXFileReference, PBXReferenceProxy]
 5    attribute :source_tree, String, '<group>'
 6    # 记录 group 在文件系统中的路径
 7    attribute :path, String
 8		# 默认情况下,如果指定了路径,则此属性不存在。
 9    attribute :name, String
10end

它提供了一个便利的初始化方法:

1def new_group(name, path = nil, source_tree = :group)
2  group = project.new(PBXGroup)
3  children << group
4  group.name = name
5  group.set_source_tree(source_tree)
6  group.set_path(path)
7  group
8end

这里的 name 是必须传的,path 则可以为空。而我们在 Xcode 的导航栏中所见的各个分组名称和路径就是由这两个属性来决定的。没有 name 的情况下则是以 path 的 basename 作为 display_name 来呈现。

最后来看个例子:

1F8E469631395739D00DB05C8 /* Frameworks */ = {
2  isa = PBXGroup;
3  children = (
4    50ABD6EC159FC2CE001BE42C /* MobileCoreServices.framework */,
5    ...
6  );
7  name = Frameworks;
8  sourceTree = "<group>";
9};

项目中并不存在真实的 Framework 文件夹,所以无需指定 path。另外注意,mainGroup 既没有 name 也没有 path,大家可以思考一下原因。

通过代码编辑 Xcode 工程

最后一章,一起来实践一下简单的功能。

为 Xcode 工程添加文件

0x01 添加源文件

1project = Xcodeproj::Project.open(path)
2target = project.targets.first
3group = project.main_group.find_subpath(File.join('Xcodeproj', 'Test'), true)
4group.set_source_tree('SOURCE_ROOT')
5file_ref = group.new_reference(file_path)
6target.add_file_references([file_ref])
7project.save

给 Xcode 工程添加文件算是常规操作。这段代码引用自 draveness 的文章,作者对每一步都做了详细的解释了。我们从代码角度重点说一下 new_referenceadd_file_references

PBXGroup::new_references

Xcode 工程会指定 main_group 作为项目资源的入口,所添加的文件资源需要先加入 main_group 内,以生成对应的 PBXFileReference,这样 target 任务和 buildPhase 等配置才能访问该资源。因此,new_reference 核心是 new_file_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_file_reference(group, path, source_tree)
16  path = Pathname.new(path)
17  ref = group.project.new(PBXFileReference)
18  group.children << ref
19  GroupableHelper.set_path_with_source_tree(ref, path, source_tree)
20  ref.set_last_known_file_type
21  ref
22end

PBXNativeTarget::add_file_references

加入文件资源后,便可将其加入对应的构建任务中去。

 1def add_file_references(file_references, compiler_flags = {})
 2  file_references.map do |file|
 3    extension = File.extname(file.path).downcase
 4    header_extensions = Constants::HEADER_FILES_EXTENSIONS
 5    # 依据资源类型区分是头文件还是源文件 
 6    is_header_phase = header_extensions.include?(extension)
 7    phase = is_header_phase ? headers_build_phase : source_build_phase
 8
 9    # 将文件资源作为编译资源加入到编译源中
10    unless build_file = phase.build_file(file)
11      build_file = project.new(PBXBuildFile)
12      build_file.file_ref = file
13      phase.files << build_file
14    end
15	 ...
16    yield build_file if block_given?
17
18    build_file
19  end
20end

0x02 添加编译依赖

这里的编译依赖是指,依赖的 framework、library、bundle。不同于文件,他有均有对应的虚拟 group。

 1# 添加 framework 引用
 2file_ref = project.frameworks_group.new_file('path/to/A.framework')
 3target.frameworks_build_phases.add_file_reference(file_ref)
 4
 5# 添加 libA.a 引用
 6file_ref = project.frameworks_group.new_file('path/to/libA.a')
 7target.frameworks_build_phases.add_file_reference(file_ref)
 8
 9# 添加 bundle 引用
10file_ref = project.frameworks_group.new_file('path/to/xxx.bundle')
11target.resources_build_phase.add_file_reference(file_ref)

大家有兴趣可以看看 CocoaPods 是如何配置 Pods.project,为 Pod 生成对应 target,入口在 Installer::generate_pods_project 处。

总结

Xcode 通过 project.pbxproj,在文件系统之上又抽象出基于 PBXFileReferencePBXGroup 的任务构建系统。对于这一操作一直是不能理解的。为什么不像其他 IDE 一样直接使用项目的文件目录,而是另起炉灶搞了一套似乎并不好用构建件系统呢 ?

在了解完 project.pbxproj 和 Xcodeproj 后,似乎能理解一些设计者的意图。

假设你参与的是一个多人合作的超大型项目 (Project),它将同时存在多个子任务 (Target),同时这些任务之间可能存在大量交叉的资源依赖。这些资源不限于 File、Framework、Target、Project。如果我们直接基于文件路径来记录这些资源及其依赖关系,那么当资源发生产生变化时,将会产生大量的修改,例如文件路径变更。

这其实是不能接受的。而有了 PBXFileReference 的存在,任务构建者只需关心 PBXFileReference 这唯一引用,无需在意它背后的文件是否存在,路径是否正确,甚至是内容是否变更,这些都不重要。通过 PBXFileReference 这层隔离,实现了任务的构建行为及资源关系的固化。

知识点问题梳理

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

  1. 说说 xcworkspacexcodeproj 本质是什么,有什么区别 ?
  2. pbxprojisa 关键字的作用,有哪些类型 ?
  3. 说说 pbxproj 中是如何定义和引用源文件的 ?
  4. pbxproj 如何映射为 Xcodeproj 中的对象的 ?
  5. 谈谈你对 PBXGroupPBXFileReference 的理解 ?