阅读 412

8. Xcode 工程文件解析

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 项目,来看看其目录结构:

Test.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│   ├── contents.xcworkspacedata
│   └── xcuserdata
│       └── edmond.xcuserdatad
│           └── UserInterfaceState.xcuserstate
└── xcuserdata
    └── edmond.xcuserdatad
        └── xcschemes
            └── xcschememanagement.plist
复制代码

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

Test.xcworkspace
└── contents.xcworkspacedata
复制代码

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

<?xml version="1.0" encoding="UTF-8"?>
<Workspace
   version = "1.0">
   <FileRef
      location = "group:Test.xcodeproj">
   </FileRef>
   <FileRef
      location = "group:Pods/Pods.xcodeproj">
   </FileRef>
</Workspace>
复制代码

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

<?xml version="1.0" encoding="UTF-8"?>
<Workspace
   version = "1.0">
   <FileRef
      location = "self:">
   </FileRef>
</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 还支持二进制数据的表示,以 <> 修饰文本形式的十六进制数,其中字典与数组的区别如下:

Array:
plist => ( "1", "2", "3" )
json => [ "1", "2", "3" ]

Dictionary:
plist => { "key" = "value"; ... }
json => { "key" : "value", ... }
复制代码

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

plutil -convert xml1 -s -r -o project.pbxproj.xml project.pbxproj
复制代码

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

project.pbxproj

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

// !$*UTF8*$!
{
	archiveVersion = 1;
	classes = {
	};
	objectVersion = 50;
	objects = {
		...
   };
	rootObject = 8528A0D92651F281005BEBA5 /* Project object */;
}
复制代码

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

53 => 'Xcode 11.4',
52 => 'Xcode 11.0',
51 => 'Xcode 10.0',
50 => 'Xcode 9.3',
48 => 'Xcode 8.0',
47 => 'Xcode 6.3',
46 => 'Xcode 3.2',
45 => '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 的生成需要依赖 👇 这些数据:

struct globalidentifier {
    int8_t user;            // encoded username
    int8_t pid;             // encoded current pid #
    int16_t _random;        // encoded random value
    int32_t _time;          // encoded time value
    int8_t zero;            // zero
    int8_t host_shift;      // encoded, shifted hostid
    int8_t host_h;          // high byte of lower bytes of hostid
    int8_t host_l;          // low byte of lower bytes of hostid
} __attribute__((packed));  // packed, so we always have a length of 12 bytes
复制代码

Xcode Object

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

PBXProject 为例:

/* Begin PBXProject section */
   8528A0D92651F281005BEBA5 /* Project object */ = {
      isa = PBXProject;
      ...
      mainGroup = 8528A0D82651F281005BEBA5;
      productRefGroup = 8528A0E22651F281005BEBA5 /* Products */;
      projectDirPath = "";
      projectRoot = "";
      targets = ( 
         8528A0E02651F281005BEBA5 /* Test */,
      );
   };
/* 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 类,用于记录根元素。

module Xcodeproj
  class Project
    # object 模块,后面会提到
    include Object	
    ...
    # 序列化时 project 的最低兼容版本, 与 object_version 对应
    attr_reader :archive_version
    # 作用未知
    attr_reader :classes
    # project 最低兼容版本
    attr_reader :object_version
    # project 所包含的 objects,结构为 [Hash{String => AbstractObject}]
    attr_reader :objects_by_uuid
    # project 的根结点,即 PBXProject
    attr_reader :root_object
  end
end
复制代码

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

01-resolve-dependencies

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

def inspect_targets_to_integrate
  project = Xcodeproj::Project.open(project_path)
  ...
end
复制代码

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

# lib/xcproject/Project.rb

def self.open(path)
  path = Pathname.pwd + path
  unless Pathname.new(path).exist?
    raise "[Xcodeproj] Unable to open `#{path}` because it doesn't exist."
  end
  project = new(path, true)
  project.send(:initialize_from_file)
  project
end

def initialize_from_file
  pbxproj_path = path + 'project.pbxproj'
  plist = Plist.read_from_path(pbxproj_path.to_s)
  root_object.remove_referrer(self) if root_object
  @root_object     = new_from_plist(plist['rootObject'], plist['objects'], self)
  @archive_version = plist['archiveVersion']
  @object_version  = plist['objectVersion']
  @classes         = plist['classes'] || {}
  @dirty           = false

  unless root_object
    raise "[Xcodeproj] Unable to find a root object in #{pbxproj_path}."
  end

  if archive_version.to_i > Constants::LAST_KNOWN_ARCHIVE_VERSION
    raise '[Xcodeproj] Unknown archive version.'
  end

  if object_version.to_i > Constants::LAST_KNOWN_OBJECT_VERSION
    raise '[Xcodeproj] Unknown object version.'
  end

  # Projects can have product_ref_groups that are not listed in the main_groups["Products"]
  root_object.product_ref_group ||= root_object.main_group['Products'] || root_object.main_group.new_group('Products')
end
复制代码
  1. 在 open 方法中会检验路径,初始化 Project 对象;
  2. 使用内部 Property List 类 Plist 来读取 project.pbxproj 数据;

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

def new_from_plist(uuid, objects_by_uuid_plist, root_object = false)
  attributes = objects_by_uuid_plist[uuid]
  if attributes
    # 以 isa 获取 Xcodeproj 中对应的 Xcode Object 类型
    klass = Object.const_get(attributes['isa'])
    object = klass.new(self, uuid)
    objects_by_uuid[uuid] = object
    object.add_referrer(self) if root_object
    # 解析 objects 节点,并完成 ISA 数据到 Xcode Object 的映射
    object.configure_with_plist(objects_by_uuid_plist)
    object
  end
end
复制代码
  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 类型。先简单介绍部分属性,方便后续理解。

class AbstractObject
  # object 类型
  attr_reader :isa
  # object 唯一标识
  attr_reader :uuid
  # 持有 object 的 project
  attr_reader :project

  def initialize(project, uuid)
    @project = project
    @uuid = uuid
    @isa = self.class.isa
    @referrers = []
    unless @isa =~ /^(PBX|XC)/
      raise "[Xcodeproj] Attempt to initialize an abstract class (#{self.class})."
    end
  end
  ...
end
复制代码

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

def new(klass)
   if klass.is_a?(String)
     klass = Object.const_get(klass)
   end
   object = klass.new(self, generate_uuid)
   object.initialize_defaults
   object
end
复制代码

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

Const Accessor

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

Module#const_get
Module#const_set
复制代码

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

# lib/xcodeproj/project/object.rb

Xcodeproj::Constants::KNOWN_ISAS.each do |superclass_name, isas|
  superklass = Xcodeproj::Project::Object.const_get(superclass_name)
  isas.each do |isa|
    c = Class.new(superklass)
    Xcodeproj::Project::Object.const_set(isa, c)
  end
end
复制代码

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

KNOWN_ISAS = {
  'AbstractObject' => %w(
    PBXBuildFile
    AbstractBuildPhase
    PBXBuildRule
    XCBuildConfiguration
    XCConfigurationList
    PBXContainerItemProxy
    PBXFileReference
    PBXGroup
    PBXProject
    PBXTargetDependency
    PBXReferenceProxy
    AbstractTarget
  ),

  'AbstractBuildPhase' => %w(
    PBXCopyFilesBuildPhase
    PBXResourcesBuildPhase
    PBXSourcesBuildPhase
    PBXFrameworksBuildPhase
    PBXHeadersBuildPhase
    PBXShellScriptBuildPhase
  ),

  'AbstractTarget' => %w(
    PBXNativeTarget
    PBXAggregateTarget
    PBXLegacyTarget
  ),

  'PBXGroup' => %w(
    XCVersionGroup
    PBXVariantGroup
  ),
}.freeze
复制代码

Object Attributes

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

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

AbstractObjectAttribute

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

class AbstractObjectAttribute
   
  # 属性本身类型,:simple, :to_one, :to_many.
  attr_reader :type
   
  # 属性名
  attr_reader :name
   
  # 属性的持有者
  attr_accessor :owner

  def initialize(type, name, owner)
    @type  = type
    @name  = name
    @owner = owner
  end

  # 属性支持的类型
  attr_accessor :classes
   
  # 仅限于 :references_by_keys 关联的类型
  attr_accessor :classes_by_key
   
  # 仅限于 :simple 类型指定默认值
  attr_accessor :default_value
  ...
end
复制代码

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

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

class C
  class << self
    puts self	# 输出:#<Class:C>,类 C 类对象的单例类
  end
end
复制代码

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

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

module Object
  class AbstractObject
    class << self
      # 普通属性声明方法
      def attribute(name, klass, default_value = nil) ... end
       
      # 单一引用的属性声明方法
      def has_one(singular_name, isas) ... end
       
      # 多引用的属性声明方法
      def has_many(plural_name, isas) ... end
       
      # 多引用且不同 key 的属性声明方法
      def has_many_references_by_keys(plural_name, classes_by_key) ... end
    end
  end
end
复制代码

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

module Object
  class PBXProject < AbstractObject     
     
    # 声明为 [String] 类型的 project_dir_path,默认值为空
    attribute :project_dir_path, String, ''
    
    # 声明为 [PBXGroup] 类型的 main_group
    has_one :main_group, PBXGroup

    # 声明为 [ObjectList<AbstractTarget>] 类型的 targets
    has_many :targets, AbstractTarget
     
    # 声明为 [Array<ObjectDictionary>] 类型的 project_references 
    has_many_references_by_keys :project_references,
                                :project_ref   => PBXFileReference,
                                :product_group => PBXGroup
    ...
  end
end
复制代码

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

Attribute

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

# lib/xcodeproj/project/object_attributes.rb

def attribute(name, klass, default_value = nil)
  # 1. 初始化 attribute,记录属性名,属性类型及默认值
  attrb = AbstractObjectAttribute.new(:simple, name, self)
  attrb.classes = [klass]
  attrb.default_value = default_value
  # 2. 将 attrb 存入 @attributes
  add_attribute(attrb)
  
  # 3. 添加名为 name 的 getter
  define_method(attrb.name) do
    @simple_attributes_hash ||= {}
    @simple_attributes_hash[attrb.plist_name]
  end

  # 4. 添加名为 #{name}= 的 setter,并将值存入 @simple_attributes_hash 字典中
  define_method("#{attrb.name}=") do |value|
    @simple_attributes_hash ||= {}
    # 5. 检查 value 是否为 klass 支持的类型
    attrb.validate_value(value)

    existing = @simple_attributes_hash[attrb.plist_name]
    if existing.is_a?(Hash) && value.is_a?(Hash)
      return value if existing.keys == value.keys && existing == value
    elsif existing == value
      return value
    end
    mark_project_as_dirty!
    @simple_attributes_hash[attrb.plist_name] = value
  end
end
复制代码

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

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

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

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

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

def project_dir_path
  @simple_attributes_hash[projectDirPath]
end

def project_dir_path=(value)
  attribute.validate_value(value)
  @simple_attributes_hash[projectDirPath] = value
end
复制代码

Xcode Object

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

PBXFileReference

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

F8DA09E31396AC040057D0CC /* main.m */ = {
  isa = PBXFileReference; 
  fileEncoding = 4;
  lastKnownFileType = sourcecode.c.objc; 
  path = main.m; 
  sourceTree = SOURCE_ROOT; 
};
复制代码

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

  • <absolute>:绝对路径

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

  • SOURCE_ROOT 基于 project 的相对路径

  • DEVELOPER_DIR 基于 developer directory 的相对路径

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

  • SDKROOT 基于 SDK directory 的相对路径

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

def source_tree_real_path(object)
  case object.source_tree
  when '<group>'
    object_parent = parent(object)
    if object_parent.isa == 'PBXProject'.freeze
      object.project.project_dir + object.project.root_object.project_dir_path
    else
      real_path(object_parent)
    end
  when 'SOURCE_ROOT'
    object.project.project_dir
  when '<absolute>'
    nil
  else
    Pathname.new("${#{object.source_tree}}")
  end
end
复制代码

PBXFileReference 主要定义如下:

module Object
  class PBXFileReference < AbstractObject
    attribute :name, String
    attribute :path, String
    attribute :source_tree, String, 'SOURCE_ROOT'
end
复制代码

PBXGroup

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

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

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

def new_group(name, path = nil, source_tree = :group)
  group = project.new(PBXGroup)
  children << group
  group.name = name
  group.set_source_tree(source_tree)
  group.set_path(path)
  group
end
复制代码

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

最后来看个例子:

F8E469631395739D00DB05C8 /* Frameworks */ = {
  isa = PBXGroup;
  children = (
    50ABD6EC159FC2CE001BE42C /* MobileCoreServices.framework */,
    ...
  );
  name = Frameworks;
  sourceTree = "<group>";
};
复制代码

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

通过代码编辑 Xcode 工程

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

为 Xcode 工程添加文件

0x01 添加源文件

project = Xcodeproj::Project.open(path)
target = project.targets.first
group = project.main_group.find_subpath(File.join('Xcodeproj', 'Test'), true)
group.set_source_tree('SOURCE_ROOT')
file_ref = group.new_reference(file_path)
target.add_file_references([file_ref])
project.save
复制代码

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

PBXGroup::new_references

Xcode 工程会指定 main_group 作为项目资源的入口,所添加的文件资源需要先加入 main_group 内,以生成对应的 PBXFileReference,这样 target 任务和 buildPhase 等配置才能访问该资源。因此,new_reference 核心是 new_file_reference 代码如下:

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_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
复制代码

PBXNativeTarget::add_file_references

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

def add_file_references(file_references, compiler_flags = {})
  file_references.map do |file|
    extension = File.extname(file.path).downcase
    header_extensions = Constants::HEADER_FILES_EXTENSIONS
    # 依据资源类型区分是头文件还是源文件 
    is_header_phase = header_extensions.include?(extension)
    phase = is_header_phase ? headers_build_phase : source_build_phase

    # 将文件资源作为编译资源加入到编译源中
    unless build_file = phase.build_file(file)
      build_file = project.new(PBXBuildFile)
      build_file.file_ref = file
      phase.files << build_file
    end
	 ...
    yield build_file if block_given?

    build_file
  end
end
复制代码

0x02 添加编译依赖

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

# 添加 framework 引用
file_ref = project.frameworks_group.new_file('path/to/A.framework')
target.frameworks_build_phases.add_file_reference(file_ref)

# 添加 libA.a 引用
file_ref = project.frameworks_group.new_file('path/to/libA.a')
target.frameworks_build_phases.add_file_reference(file_ref)

# 添加 bundle 引用
file_ref = project.frameworks_group.new_file('path/to/xxx.bundle')
target.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 的理解 ?
文章分类
iOS