View in English

  • Apple 开发者
    • 入门汇总

    探索“入门汇总”

    • 概览
    • 学习
    • Apple Developer Program

    及时了解最新动态

    • 最新动态
    • 开发者你好
    • 平台

    探索“平台”

    • Apple 平台
    • iOS
    • iPadOS
    • macOS
    • Apple tvOS
    • visionOS
    • watchOS
    • App Store

    精选

    • 设计
    • 分发
    • 游戏
    • 配件
    • 网页
    • Home
    • CarPlay 车载
    • 技术

    探索“技术”

    • 概览
    • Xcode
    • Swift
    • SwiftUI

    精选

    • 辅助功能
    • App Intents
    • Apple 智能
    • 游戏
    • 机器学习与 AI
    • 安全性
    • Xcode Cloud
    • 社区

    探索“社区”

    • 概览
    • “与 Apple 会面交流”活动
    • 社区主导的活动
    • 开发者论坛
    • 开源

    精选

    • WWDC
    • Swift Student Challenge
    • 开发者故事
    • App Store 大奖
    • Apple 设计大奖
    • Apple Developer Centers
    • 文档

    探索“文档”

    • 文档库
    • 技术概述
    • 示例代码
    • 《人机界面指南》
    • 视频

    发布说明

    • 精选更新
    • iOS
    • iPadOS
    • macOS
    • watchOS
    • visionOS
    • Apple tvOS
    • Xcode
    • 下载

    探索“下载”

    • 所有下载
    • 操作系统
    • 应用程序
    • 设计资源

    精选

    • Xcode
    • TestFlight
    • 字体
    • SF Symbols
    • Icon Composer
    • 支持

    探索“支持”

    • 概览
    • 帮助指南
    • 开发者论坛
    • “反馈助理”
    • 联系我们

    精选

    • 《开发者账户帮助》
    • 《App 审核指南》
    • 《App Store Connect 帮助》
    • 即将实行的要求
    • 协议和准则
    • 系统状态
  • 快速链接

    • 活动
    • 新闻
    • 论坛
    • 示例代码
    • 视频
 

视频

打开菜单 关闭菜单
  • 专题
  • 所有视频
  • 关于

更多视频

  • 简介
  • 转写文稿
  • Xcode 构建过程的幕后秘密

    有没有好奇过 Xcode 在构建项目时到底发生了什么?了解 Xcode 如何自动完成构建 app 所需的各个步骤,并走进幕后来了解 clang、swiftc 和 linker 如何相互配合,将您的源代码变成可以运行的程序。

    资源

      • 高清视频
      • 标清视频
    • 演示幻灯片 (PDF)

    相关视频

    WWDC22

    • 深入探索 Xcode 构建中的并行

    WWDC20

    • 改进 Swift 的 Objective-C 框架

    WWDC18

    • 在 Xcode 中更快地构建
    • Swift 新功能
  • 搜索此视频…

    (Xcode构建过程的后台工作 演讲415)

    大家下午好 欢迎来到 Xcode构建过程的后台工作 我是Jake Petroules 我是Xcode构建系统团队的 工程师 今天我们要讨论 Xcode构建过程

    首先我要讲讲 Xcode 10的新构建系统 用Swift的scratch编写 提供更好的性能和可靠度 我们要解答如下问题 按住Command + B 会发生什么 如何架构构建过程 Xcode如何用 project文件的信息 决定构建过程的模型和流程

    然后是编译器领域 看看Clang和Swift 怎样将源代码加入目标文件 我们会展示 头文件和模块的运行 编译器如何在代码中查找声明 以及Swift编译模型如何不同于 C、C++ 和Objective-C

    最后是连接器 构建过程的最后一步 我们会解释什么是符号 及其与源代码的关系 还有连接器如何将 编译器产生的目标文件 整合成最终的可执行文件 提供给app或框架

    我们会使用 叫PetWall的小app样本 作为这次演讲的示例 它只是一个很小的iOS app 用来展示宠物图片

    首先来了解 什么是构建过程 以及如何用Xcode 创建类似PetWall的app 这里你看到app对象、框架 不同的代码文件 都是Swift 和Objective-C 看起来可能很像你自己的项目

    在构建app的时候

    要经过很多步骤 从源代码 和项目资源开始 到提供给客户的打包文件 或上传到App Store发布 你要编译和连接源代码 复制和处理资源 比如头文件 资源目录和Storyboard 最后是代码签名以及自定义 shell脚本或make文件 比如给框架构建API文件 运行代码检查和验证工具等

    大多数任务 在构建过程中 由命令行工具运行 比如Clang、LD、AC工具 IB工具、代码符号等 这些工具的执行 需要一组特定的实参 以特定的顺序 基于Xcode项目的配置

    构建系统的用处 就是将任务的执行部署自动化 在每次构建的时候 由于任务数量成千上万 构建过程更是数不胜数 依赖关系十分复杂 你肯定不想手动输入 1天敲100遍命令

    那就让构建系统来做

    我说过 构建系统里的任务 按照特定的顺序进行 因此我们谈谈这个顺序 怎么决定及其重要性

    构建任务的执行顺序 取决于信息的依赖关系 就是任务 任务需要的输入 和任务生成的输出 以编译任务为例 它需要输入源代码文件 比如PetController.m 然后输出目标文件 比如PetController.o 同样 连接器任务 需要几个目标文件 由编译器在上个任务中生成 再生成可执行或lib文件 比如PetWall运行文件 会存到.app资源文件包 希望大家已经看出些许端倪 你能看到信息的依赖关系 是顺着这个图形的走向 最终形成执行顺序 现在大家关注下 图形中的编译任务 很像川流不息的马路 你看得到编译任务 在各自的路上互不干涉 可以平行运行 因为连接器任务 需要所有其他的输入 它要在最后一位

    构建系统通过依赖关系 决定任务执行的顺序 以及平行运行的任务 我们叫做依赖顺序 知道了构建过程的定义后 我们具体讲讲怎么操作 点击“构建”会发生什么? 构建系统的第一步是获取构建描述 Xcode项目文件 解析项目中的所有文件 目标app和依赖关系 构建设置 转换成一个树形结构叫做定向图 它显示了所有依赖关系 项目中的输入和输出文件 以及处理它们的执行任务

    然后低级执行引擎会处理这张图 研究依赖关系 决定执行哪个任务 执行顺序是什么 以及哪些可以平行运行 然后继续执行任务 我说的低级执行引擎 是新构建系统的 叫做llbuild 它是开源资源 用GitHub开发 如果对构建系统开发感兴趣 请随意研究 看看它如何工作 它的链接和另一个关于构建系统的 开源模块 会在演讲最后提到

    现在讲讲已知的依赖关系 由于你无法获取过多的依赖关系信息 构建系统可能会找到更多 在任务的执行过程中 比如Clang编译 Objective-C文件时 如你所料 会生成目标文件 但是它也会生成另一个文件 其中包含一个列表 列出源文件中的头文件 那么下次构建时 构建系统会使用这个文件中的信息 以保证再次编译源文件 如果你更改了其中任何头文件的话 这里的关系路径是 PetController.h PetController.d .n直到.o文件

    我们已经讲了很多 构建系统的主要工作就是执行任务 当然项目越大 构建时间越长 你肯定不想把所有任务都运行一遍 在每次构建的时候 构建系统实际上可以只执行 定向图上的任务子集

    基于你对项目做出的更改 对比上次构建过程 我们称之为累加构建 准确的依赖关系十分重要 这样累加构建才能正确高效地工作

    下面看看哪些更改 会影响构建系统 以及与累加构建的关系 构建系统如何检测更改呢?

    构建过程中的每个任务 都有相应的签名 类似于Hash 通过计算 多个任务相关信息而得出

    这些信息包括 任务输入的统计信息 比如文件路径 和更改时间标签 运行命令的命令行指示 以及其他有关任务的元数据 比如编译器版本

    构建系统会追踪任务签名 包括当前和之前的构建 所以它知道每次构建时 是否要重新运行任务

    如果某个任务的签名 与上次构建时不同 它就会重新运行这个任务 如果相同就会跳过 这就是累加构建的概念

    我们大概了解了 构建过程的定义和流程 如何利用构建系统呢?

    先回顾下基本知识 构建系统按照一定顺序 执行一系列任务 但要记得 构建过程以定向图表示

    我们不要担心 任务执行的顺序 这是构建系统的工作 作为开发者 我们要考虑的是 任务之间的依赖关系 让构建系统决定 最佳的执行方法 根据定向图结构 这样构建系统 可以正确地给任务排序 可能的时候并排运行 以完全利用多核硬件

    依赖关系源自哪里呢? 对某些任务 依赖关系 来自构建系统自带的数据 构建系统自带一些规则 比如编译器、连接器 资源目录 Storyboard处理器等等 这些规则定义了 哪些是输入文件 和哪些是输出文件

    还有目标依赖关系 大致决定了 目标构建的顺序 有些时候 构建系统可以编译 不同目标和平行文件 之前的Xcode 要构建一个app 就要完成整个app的构建 然后才能使用 Xcode X的新构建系统 就要快得多 编译源阶段会提前开始 免费提供并行 但是如果含有 任何运行脚本阶段 这些阶段完成后 并行才能开始

    有关依赖的还有隐性依赖关系 比如在链接库里 列出一个目标 用二进制构建阶段 隐性依赖关系 由scheme编辑器生成 默认为开启 构建系统会为这个目标 建立隐性依赖关系 即便它不在目标依赖关系之列

    接下来是构建阶段依赖 在目标编辑器里 你会看到几个构建阶段 复制头文件、编译源 复制资源包等等 这些任务与每个阶段相关 通常按组运行 根据阶段的排列顺序 构建系统也会忽略它 如果有更好的方法 比如第三方静态库阶段 在编译源之前 注意有的时候 构建阶段顺序不对 会导致问题或构建失败 因此为了解依赖关系 验证构建阶段的顺序

    还有scheme顺序依赖 如果开启了 并行构建检查 在方案设置里 构建性能会更好 不用担心目标顺序 但如果关闭 并行构建 Xcode构建目标时 会按照你排列的 构建行动顺序 逐个构建 目标依赖关系 优先级较高 优先决定 第一个构建目标 但Xcode会遵从这个排列 这个让人跃跃欲试 因为它给出了可预期的构建顺序 即使依赖关系有误 但这样会牺牲 大量并行空间 延缓构建速度 所以我们推荐 开启并行构建 正确设置依赖关系 不要依赖排序

    最后 依赖关系在于你自己 开发者

    你可以自定义shell脚本 构建阶段或规则 明确告诉构建系统 输入和输入都是什么 以避免重复运行 不必要的脚本任务 保证正确执行顺序 你可以用运行脚本阶段编辑器 定义输入和输出 这些文件的路径 将作为环境变量 在脚本中激活

    不要依赖项目里 目标依赖关系的自动连接 Clang编译器 有自动关联功能 在构建设置中 自动使用关联框架 让编译器自动连接框架 对应导入的模块 不用在连接库的 构建阶段再明确表示 但是要注意 自动关联 不会建立依赖关系 在构建系统层级 所以它不能保证 依赖的目标 在关联之前已经建好

    所以它只能用于平台STK的框架 比如 Foundation或UIKit 因为我们知道 它们在构建前就已经存在 你自己项目里的目标 要保证明确的库依赖关系

    你也许需要创建项目引用 将另一个Xcode项目拖放到 项目文件导航 说明与其他项目的 目标文件的依赖关系

    总结来说 有了准确的依赖关系 构建系统就能 更好地并行构建任务 保证每次构建的结果一致 这样就能减少构建用时 给开发多点时间 想知道更多快速构建的内容 如何最大化利用 崭新的iMac Pro内核 推荐观看演讲 用Xcode加速构建过程 现在 我要有请Jurgen 他会带大家 探索编译器的世界

    谢谢 Jake

    现在我们要看看 后台都发生了什么 当Xcode构建系统 启动Clang编译器的时候

    大家好 我是Jurgen 我是Clang前端团队的 编译器工程师 今天我要讲两个功能 大家可能已经知道了 第一个是头文件映射 我们用它传递信息 从Xcode构建系统 到Clang编译器

    第二个是Clang模块 我们用它加快构建的速度

    在坐有些人可能只用Swift 我想告诉你 Swift在后台也用Clang 所以你们应该也会感兴趣

    什么Clang?

    Clang是Apple的 官方编译器 用于所有C语言 比如C、C++ 当然还有Objective-C 大部分框架都在用的语言

    Jake刚才提过

    编译器一次性 编辑所有输入文件 生成仅一个输出文件 之后被连接器使用 如果要从OS访问API 或从自己的代码 访问实现文件 就需要一个叫做 头文件的东西

    头文件是一种承诺 承诺在其他地方 存在这个实现文件 它们通常可以匹配

    如果你只更新实现文件 而忘记头文件 你就食言了 通常这个问题 不会在编译过程中出现 因为编译器相信你的承诺 问题出在链接过程

    编译器通常包含 不止一个头文件 而且所有编译器 都是这样被调用 以样本app为例 看看怎么处理头文件

    这个PetWall 是多语言app app本身 用Swift编写

    框架的语言是 Objective-C 它有个兼容库 美学档案 是C++语言

    时间越长 app内容越多 所以要重新组织 方便查找文件 比如 将所有跟猫有关的文件 移动到子文件

    不要改变任何实现文件 也是可以的 那么你想

    Clang是如何找到头文件的

    举个简单的例子 这是一个实现文件 在这个代码里 包含一个头文件 命名为cat.h

    怎么知道Clang做了什么 一是查看构建日志

    看看Xcode构建系统 在编译这个文件时做了什么 复制粘贴这个调用代码 打开命令窗口 输入-v选项 -v代表显示 然后Clang会返回很多信息 我们只要关注一个 搜索路径

    我说搜索路径 大家想到的可能是 指向源代码的搜索路径

    不是这样的 相反 你会看到这个 headermaps (寻找猫咪) Headermaps 由Xcode构建系统创建 说明头文件的位置

    仔细看下 这是最重要的两个 headermap文件

    前两行只是 插入了框架名称 给头文件 这两个头文件 原来是公共文件

    我建议 不要依赖这个功能 原因是…

    把它放在这里 是为了让现有项目持续运行 但之后使用Clang模块时 可能会出现问题 所以我们建议 自己标出框架名称 在将公共或私有头文件 从自己的框架中导入时

    第三行是项目头文件 这个例子中并不需要 而且… headermap是为了 连接回源代码

    如你所见 公共和私有头文件的操作一样 总是回归源代码 这是为了让Clang生成 有用的报错信息 用于源目录文件 而不是从构建目录 复制过来的 其他内容

    很多人不知道 头文件映射的存在 就会遇到某些问题 最常见的是 忘记将头文件 添加到项目 它在源目录 但不在项目里 一定保证将头文件 添加到项目 另一个问题是

    如果头文件命名一样 它们会彼此重叠

    所以头文件的名称 要有所区别 这对系统头文件也一样 如果项目里的本地头文件 与系统头文件名字一样 它会叠加到系统头文件上 要注意避免 提到系统头文件 怎么查找?

    再用PetWall演示下 这里导入 Foundation.h头文件 在SDK里

    我们可以像之前一样 就像查找自己项目的头文件 但现在是系统头文件 我说过头文件映射 只用于私有的头文件 所以可以忽略 现在关注导入路径 那么 默认的SDK里有两个目录 第一个是用户的 第二个是系统库框架 先看第一个

    这是正常的包含目录 我们只要输入搜索关键词 这里是 Foundation/Foundation.h 头文件没找到 因为它不在那里 没关系 再试试第二个 系统库框架

    这是框架目录 所以Clang的做法会有些不同 首先… 它要确定框架的定义 看看框架是否存在

    然后 从头文件目录中 查找头文件 这里找到了 不错 但如果没找到呢? 比如输入不存在的假冒头文件

    显然不会在头文件目录中找到

    但之后它会查找 私有头文件目录

    Apple的SDK中 不会带有任何私有头文件 但是你的项目 和框架中可能公共和私有头文件都有 所以也会检查 但这是个假的头文件 所以那里也没有

    现在就有意思了 搜索停止了 我们不会再查找其他目录 这是因为 框架已经被找到了 一旦框架被找到 一般框架目录里 能找到头文件 如果找不到 搜索就放弃了

    如果好奇 实现文件的模样 当所有头文件 都被导入和预处理之后 你可以让Xcode 创建预处理文件 给实现文件

    这会生成一个 巨大的输出文件

    有多大呢?

    举个简单的例子

    Foundation.h是… 系统最基本的头文件

    这个头文件很可能被 直接或间接地 导入其他头文件夹 就是说… 每次调用编译器 都要查找这个头文件

    一天之内

    Clang要查找并处理 800多个头文件夹 只为一个导入声明

    也就是要解析和验证 大于9兆的源代码 每次调用编译器 都是如此 这个工作量很大 很冗余 所以

    怎么改善?

    这里有个功能叫做 预编译头文件 这是个好办法 但还有更好的 几年前 我们发布了Clang模块

    Clang模块只允许我们 为框架查找和解析头文件 一次 然后储存到硬盘 缓存并可以再利用 这可以提升构建速度

    要实现它 Clang模块 要具备特定的属性

    最重要的一点是 上下文无关 什么是上下文无关?

    这有两个代码片段 两个都导入了PetKit模块 但两个宏观定义不同

    如果用传统方法 导入头文件 文本也会被导入 预处理器… 会遵从这个定义 并应用到头文件夹 如果这样做 每个案例的模块 就不一样 不能重复使用 所以… 如果要使用模块 就不能这样做 模块会忽略 所有文本信息 这样就能被 所有实现文件重复使用了

    另一个要求是

    模块各自独立 也就是说 要明确所有依赖关系

    这对你有好处 就是只要你导入一个模块 它就能工作 不用考虑还要 添加其他的头文件 才能运行

    我们怎么知道 或者说Clang怎么知道 要不要创建模块呢?

    看个简单的例子 NSString.h

    首先Clang要找到框架里的 这个头文件

    你们知道怎么做了 这是Foundation.framework 目录 然后Clang编译器 会查找模块目录 和模块映射 它与头文件目录相关 找到了

    什么是模块映射? 模块映射描述了 特定的一组头文件夹 翻译到模块中

    具体讲下

    模块映射很简单 这是Foundation的 整个模块映射 就这些

    显然它描述了 模块的名称 就是Foundation 它还说明了 哪个头文件属于该模块 这里只有一个头文件 Foundation.h 但这个头文件很特殊 这是umbrella header 用特殊关键词 umbrella标出 这是说… Clang要查找 这个特殊的头文件 来确定NSString.h 是不是模块的一部分

    耶 找到了! 好了 我们已经确定 NSString.h是 foundation模块的一部分 Clang可以升级文本输入 到模块输入 为此我们要创建 foundation模块

    如何创建 foundation模块呢? 首先 为Clang单独创建位置 Clang位置里 包含的所有头文件 都属于foundation模块

    我们不会转移任何现有的上下文 来自原始的编译器调用 所以说是上下文无关 实际上我们转移的 是传递给Clang的命令行实参 随后继续传递

    在创建foundation模块时

    模块本身或框架 框架本身… 会导入其他框架 也就是说… 我们也要构建那些模块

    我们不能停顿 因为它可能还包含 其他框架 但是我们已经能看到 它的好处了 某些导入可能是一样的 所以总能重复使用那个模块

    所有模块要序列化 存到模块缓存区

    我说过…

    命令行实参会向后传递 在创建模块的时候 就是说… 这些实参会影响 模块的内容 所以我们要散列 这些实参 再保存这些 为特定编译器调用而创建的模块 到散列匹配的目录里

    如果修改编译器实参 用不同的限制文件 比如写入 enable cat

    这是不同的散列 要求Clang重新创建 所有模块 且输入到那个目录 匹配那个散列

    为了更多重复利用模块缓存 你要保证实参的一致性 如果可能的话

    以上就是如何查找 和创建系统框架模块 但是你的框架呢? 如何为它们创建模块?

    回到刚才猫的例子 这时打开模块

    如果要用头文件映射 它会映射到源目录

    看看这个源文件 出问题了 这里没有模块目录

    看上去根本不是框架

    Clang现在不知所措 答案是一个新的概念 叫做 Clang的虚拟文件系统 它会创建一个 虚拟的抽象框架 方便Clang创建模块 但是… 抽象框架只能映射到目录文件 这样 Clang就能在源代码中报错

    这就是创建模块的方法 在使用框架的情况下

    大家记得 开始的时候我说过 如果不确定框架名字 会有问题 我们就来举个例子 看看是什么问题

    这是很简单的代码样本 只有两个输入 第一个输入 PetKit模块

    第二个输入 我们都知道 这是PetKit模块的一部分 但Clang可能不知道 因为你没有写明 框架的名称

    这样一来 你可能会收到 重复定义的报错 这种情况常见于 将同一个头文件导入了两次

    Clang在后台辛苦工作 就为了解决 诸如此类的常见问题

    但它不可能全部解决 这只是个简单的例子 稍微调整一下

    修改一下上下文

    模块的导入 完全不受影响 因为我说过 上下文可以忽略

    cat导入 还是头文件的文本导入 它会遵循这个修改 这时可能就不是双重定义了 而是矛盾定义 无法解决 Clang解决不了 那么… 记住我的建议 永远明确框架名称 无论是导入公共的 还是私有的头文件

    现在有请Devin 他为大家讲解Swift和 Swift对Clang模块的使用

    谢谢 Jurgen

    我们现在要具体讲解 Swift和构建系统 如何并肩作战 在项目中查找声明

    先总结下 Jurgen的演讲 Clang单独编译 每个Objective-C文件 如果你要在另一个文件夹 查找一个类 你要导入声明那个类的头文件

    Swift的设计不需要写入头文件 为了方便初学者上手应用 避免了在不同文件里 重复一个声明

    但这就意味着编译器 要做些额外的记录工作 下面就讲讲 记录工作怎么做

    回到PetWall app 在界面控制器里 有个Swift的界面

    Objective-C app代理 和Swift单元测试

    为了编译 哪怕只是上面这个 PetViewController 编译器也要 进行4个不同的运算

    首先要找到声明 Swift目标里的 和来自Objective-C的

    它还要生成接口 描述文件内容 以便声明可以 被找到并用于 Objective-C 和其他Swift目标

    接下来的部分 我会演示这个例子 分别阐述这四个任务 首先是在Swift目标里查找声明

    要编译 PetViewController.swift 编译器会查找 PetView的初始程序类型 以便检查调用

    但在此之前 它要解析 PetView.swift 并验证 以保证初始程序的声明 是正常的 编译器很聪明 它知道 不需要检查 初始程序的主体 但它还要做些工作 处理文件的接口部分

    这与Clang不同 编译一个Swift文件 编译器也要解析 目标中所有其他Swift文件 以检查与接口有关的部分

    在Xcode 9 这会导致重复工作 在增量调试构建中 因为编译器单独编译每个文件 文件的编译可以并行 但它强制编译器 重复解析每个文件 解析一次 作为实现文件生成.o 解析多次是作为接口 查找声明

    Xcode 10减少了这种消耗 通过将文件合并成组 尽可能多得分担工作

    在依旧最大化并行的同时 在组中重复利用解析 只在跨组处理时重复 由于组的数量相对较少 就能大幅提升 增量调试构建的速度

    Swift代码 不止调用其他Swift代码 还能调用Objective-C

    回到PetWall样本app 我们看到它很重要 因为它是系统框架 比如 UIKit 是Objective-C语言

    Swift与其他语言不一样 它不需要外部功能接口

    这里你一般要 比如 编写Swift声明给每个 Objective-C API

    但是 编译器内置了 Clang的一大部分 用作库 这就可以直接导入 Objective-C框架

    Objective-C声明 来自哪里? 导入器会查看头文件 根据目标类型

    任何目标在导入 Objective-C框架时 导入器在头文件中 找到声明 显示的是 Clang对此框架的模块映射

    用Swift和Objective-C 代码混编的框架 导入器在umbrella头文件中 查找声明

    这个头文件定义了公共接口 这样 框架内的Swift代码 就可以调用同一框架内的 公共Objective-C代码

    最后 在app和单元测试中 可以导入目标的桥接头文件 允许其中的声明 被Swift调用

    现在… 导入器找到声明后 通常会修改它们 让它们变得更口语化 比如它会导入 Objective-C函数 用NSError惯用语 作为throwing函数 使用Swift内置的 错误处理语言功能

    具体来说 就是输入参数类型名称 后跟动词和介词

    比如函数 drawPet atPoint 带有宠物这个词 对于参数类型 宠物 后跟一个动词 画 同样单词point 代表参数类型 CGPoint 后跟介词at

    Swift删除了这些词 只导入函数 draw at

    怎么做到的? 你可能不知道 编译器带有一组 常用英文动词和介词列表

    因为它们很难编码 人类语言太复杂 有时会缺词 另外为了 匹配Swift名字转换 导入器会重命名函数 根据词性删除单词

    比如 动词feed不在列表上 所以feedPet导入后 不是我们预想的feed

    这个时候 可以用注解 NS_Swift_Name 让编译器导入 你要的函数形式

    如果你要看看 Objective-C头文件 如何导入Swift 你可以到 Xcode相关项目弹窗 它在源编辑器的左上角 选择生成的接口 就能看到接口的样子 和不同的Swift版本

    以上就是用Swift 导入Objective-C 反过来呢? Objective-C 怎么导入Swift?

    答案是 Swift会生成一个头文件 可以进行导入 这样你可以用Swift编写类 然后用Objective-C调用它们

    看看工作原理

    编译器生成 Objective-C声明 给Swift类 NSObject和@objc函数

    单元测试的app 头文件会包含 公共和内部两种声明 就能在app的Objective-C部分 使用内部Swift

    但是对于框架 生成的头文件只包含公共声明 因为它包含于构建的产品 是框架公共接口的一部分

    右边 你看到编译器 将Objective-C类连接到 名字变形的Swift类 包含模块名称 PetWall

    现在我讲一讲模块 之后Louis会讲解 命名修饰 现在大家要知道 它能防止 运行时间里的冲突 不让两个模块定义 同名的类

    你可以让Swift重命名 Objective-C类 通过传递识别符 到objc属性 如果这样做 你要保证两个名字不冲突

    我用了PWL前缀 防止冲突 这样就能在Objective-C中 引用这个类PWLPetCollar

    编译器用类似的方法 生成其他Swift目标的接口

    为此 Swift基于Clang的 模块概念进行构建 就像Jurgen说的 然后更深层的融入语言

    Swift里 模块是可分布的声明单元 为了使用这些声明 就要导入模块

    你可以导入 Objective-C模块 比如XEtest Xcode里 每个Swift目标 生成单独的模块 包括目标app

    所以要导入 app的主模块 以便进行单元测试

    导入模块时 编译器反序列化 一个特殊的Swift模块文件 在使用时 检查它的类型

    例如在单元测试中 编译器加载 PetViewController 从PetWall Swift模块里 以保证控制器的创建 没有问题

    这类似于编译器在目标里查找声明 我之前展示过的 除此之外 编译器会加载一个 总结模块的文件 而不是直接解析Swift文件

    编译器生成的 Swift模块文件 很多就像 Objective-C头文件 但它不是文本模式 而是二进制形式 它包括内联函数的主体 很像Objective-C 静态内联函数 C++头文件实现 但是 要注意一点 就是它包含 私有声明的名称和类型 这让你可以 在调试器中引用它们 很方便 但也意味着 你不能用私人秘密 来命名私有变量

    对累加构建 编译器生成 部分Swift模块文件 然后合并为一个文件 代表整个模块的内容

    合并过程可能会 生成一个 Objective-C头文件

    很多时候 这类似于连接器的操作 把目标文件整合成 一个执行文件 更多关于它的内容 有请Louis讲讲连接器 Louis 谢谢 Devin

    我是Louis Gerbarg 工作重点就是连接器 这是完成Xcode构建的最后一步 开始吧

    首先浏览一下我要讲的内容 我会讲什么是连接器 还有它用做输入的 dylibs和目标文件及其定义 还会讲到符号 及其内容 最后我会总结一遍 通过举例 因为内容比较难懂 如果感到疑惑 坚持住 希望我能讲明白

    那么… 什么是连接器? 我说过 它是最后一个 构建阶段 我们要合并 所有的.o文件 是两个编译器之前生成的 合成一个可执行文件

    全部内容就是 移动和打包代码 它不能生成代码 这点很重要 我来举例说明

    我们有两种输入文件 第一种是dylibs 就是库 有多个… 第一个应该是 目标文件 产生于构建过程 第二种是库 由多种类型组成 包括dylibs tbd .a文件或静态库

    符号是什么? 符号是名称 代表代码或数据片段

    这些片段可能会 指向其他符号 当一个函数 调用另一个函数

    符号具有属性 会影响连接器的行为 这有很多 我只举一个例子 弱符号 弱符号的注释是指 它可能会消失 当你在系统上运行 执行文件时 这都是可用性标记 表示这个API可用于iOS 12 那个API可用于iOS 11 这就引到了 今天的主题连接器 连接器决定哪些符号 必须出现 和哪些符号 可以在运行时间处理

    语言可以将数据编码成符号 通过命名修饰 Devin已经提到过

    而且… C++和Swift中 都能见到

    那么… 符号就是这些 代码和数据的名字 那么… 编译器生成目标文件 目标文件就是 代码和数据的集合

    它们不可执行 因为是编译的代码 所以还没完成 还有缺失 就需要linke整合和修复

    每个文件的片段 以符号表示 那么… print f函数 就以符号代替代码 对于其他PetKit函数 一会儿会演示 也是一样

    片段可能引用未定义符号 如果.o文件

    引用另一个 .o文件的函数 这个.o文件就是未定义的 连接器会查找未定义符号 进行匹配

    那么… 我说过目标文件是编译器的输出 什么是库? 库是定义符号的文件 但不属于构建的目标 我们有动态库 而且… 这些Mach-O文件 显示代码和数据片段 供可执行文件使用 它们是系统的一部分 这是我们用的框架 你们可能会用 自己的框架

    还有TBD文件 基于文本的动态库文件 这是什么呢? 就是…

    在给iOS和macOS 创建SDK的时候 会有所有这些动态库和函数 例如MapKit和WebKit 你可能会用到 但我们不想把 所有这些跟SDK一起加载 这样体积太大 编译器和连接器 都不需要 它只要运行程序 因此我们创建了 stub dylib 删除了所有符号的主体 只保留名字 完成之后 转用文本表示 用起来比较简单 目前它们只用于 分配SDK以减少体积

    如果在项目中看见它们 不要紧张 它们只是符号

    最后是静态库 那么… 静态库是… 之前用AR工具创建的 .o文件的集合 也可能是lib 这是它的包装工具 根据AR操作文档 AR创建并维护文件组 将它们合并为一个库 听上去像是 TAR或ZIP文件 的确是这样 事实上… .a格式是原始的库格式 在更好的工具产生之前 为UNIX所用

    但现在的编译器和连接器 可以完全理解它们 所以继续使用 它就只是个档案文件

    值得注意的是 它们孕育了动态链接 在过去 所有代码都被存档 因此 不能涵盖所有C库 只用一个函数 因此 行为是… 如果.o文件含有符号 我们会把整个.o文件 从库中提出来 但不会带入其他.o文件 如果在之间引用符号 只要带入即可 如果是非符号行为 比如静态初始程序 或将它们以你个人 dylib的形式重新导入 你要明确地用到 强制加载或制定加载 让连接器提取所有 或这些文件 即便之间没有关联

    我们通过一个例子 串联起这些内容

    台上是playSound函数样本 只看宠物不听声音 有何乐趣呢?

    那么… 调用playSound 这个Cat函数 调用playSound 很简单吧 来看看生成的程序集

    那么… 输出文件是cat.o 现在… 我们可以看到

    字符串purr.aac 就是AAC声音文件 它被复制到cat.o 你会看到 名字为purr的文件不见了 因为它是静态的 如果你熟悉C语言 这是非导出命名 没有人能引用它 既然如此 我们不需要它了 排除掉

    然后可以看到 Cat purr变成了符号 -

    跟预想的差不多

    然后我们要把这个变量 传递到playSound 这里出现了两个指令 这是因为… 我们不知道 这个字符串最后在 执行文件的位置 没有具体的位置 但我们知道RM64 就是这个程序集 它最多接收两个指令 编译器留下两个指令 留下了符号偏离 值为PAGE和PAGEOFF 连接器之后回来修复 既然已经 将字符串加载到x0

    就可以调用playSound 不用写入playSound 我们写入 __z9playSoundPKc

    这是什么? 这是变形的符号 如果仔细看 会看到cat.mm 这是Objective-C++ playSound实际是 C++函数 那么… 如果你不熟悉

    可以在命令窗口 输入命令 如果运行 Swift-demangle 传递符号 然后反修饰 没有用 它不是Swift符号 但C++filt C++反修饰器 告诉我们它实际上是 playSound的符号 除了playSound 它还有一个实参 就是const char* 因为C++会将更多信息 编入修饰符号

    现在有了.o文件 实际构建中会有更多

    那怎么办呢?

    首先 构建系统 会将所有的.o 输入到连接器 连接器会创建文件夹 放置这些文件 这里构建的PetKit 是PetWall的内嵌框架

    所以我们只要复制 创建一个文本片段 用来保存 app相关的所有代码 然后复制cat.o到这里 但是要分成两个部分 一个给字符串 一个给执行代码 现在已知它们的文件位置 连接器就能复写cat.o 基于特定的偏移值 你看到第二个指令消失了 它被一个null指令代替 没有任何行动 但是它不能被删除 因为我们无法创建或删除代码 这会打乱所有已经完成的工作 所以与其删除 不如用零行动替代 最后是分支

    那么分支指令…

    然后…

    要怎么做? 因为这个未定义符号? 我们要继续浏览 所有已经导入的.o文件

    首先是过去的静态库 这是PetSupport.a PetSupport.a里面 有一些文件 包括PetSounds.o

    大家能看到 playSound对应的符号

    把它拉入

    PetCare.o 不能被拉入 因为这个.o文件没有符号 能被app的其他部分 引用

    所有我们把它拉入 现在需要

    _open 但没有定义 你看到

    拉入的对话已变成 open$stub 为什么呢? 因为我们发现open的复制

    在lib系统的 TBD文件里

    我知道 这不属于系统库 我不会复制到 我的app 但我需要… 在app里放入足够的信息 以方便调用 所以我们创建假函数 只是个模板 用来代替 从lib系统 拿走的函数 这里就是open 观察这个函数 它实际是来自指针 open$pointer 然后跳过来 这需要一个函数指针 任何正常的C语言函数指针

    然后创建它 在数据片段中 如果有全局变量 就会出现在这里 但这里是0 如果空引用就会导致崩溃

    所以添加一个 LINKEDIT部分 LINKEDIT是元数据 连接器会用它 给操作系统留下信息 这就是动态连接器 在运行时间解决问题 了解更多信息 请观看2016年的演讲 优化app启动时间

    那么… 简单回顾下今天的内容 Jake讲述了构建系统 如何用依赖关系 优化多核构建过程

    Jurgen展示了Clang 和如何查找头文件 以及如何优化模块构建

    Devin简述了 Swift对模块的扩展 所有这些今年的新实施 比如BF处理 都是为了提升构建速度 最后连接器接收 两个编译器的输出 将它们带入app 这时Xcode会 代码签名并打包 与其他…

    app部分糅合 准备分发

    那么…

    这些基本都是开源的 如果感兴趣 可以研究Swift或Clang 或llbuild执行引擎

    对这些URL 谢谢大家的到来 而且… 希望你们享受本次WWDC 我们将在实验室见 谢谢

Developer Footer

  • 视频
  • WWDC18
  • Xcode 构建过程的幕后秘密
  • 打开菜单 关闭菜单
    • iOS
    • iPadOS
    • macOS
    • Apple tvOS
    • visionOS
    • watchOS
    打开菜单 关闭菜单
    • Swift
    • SwiftUI
    • Swift Playground
    • TestFlight
    • Xcode
    • Xcode Cloud
    • SF Symbols
    打开菜单 关闭菜单
    • 辅助功能
    • 配件
    • Apple 智能
    • App 扩展
    • App Store
    • 音频与视频 (英文)
    • 增强现实
    • 设计
    • 分发
    • 教育
    • 字体 (英文)
    • 游戏
    • 健康与健身
    • App 内购买项目
    • 本地化
    • 地图与位置
    • 机器学习与 AI
    • 开源资源 (英文)
    • 安全性
    • Safari 浏览器与网页 (英文)
    打开菜单 关闭菜单
    • 完整文档 (英文)
    • 部分主题文档 (简体中文)
    • 教程
    • 下载
    • 论坛 (英文)
    • 视频
    打开菜单 关闭菜单
    • 支持文档
    • 联系我们
    • 错误报告
    • 系统状态 (英文)
    打开菜单 关闭菜单
    • Apple 开发者
    • App Store Connect
    • 证书、标识符和描述文件 (英文)
    • 反馈助理
    打开菜单 关闭菜单
    • Apple Developer Program
    • Apple Developer Enterprise Program
    • App Store Small Business Program
    • MFi Program (英文)
    • Mini Apps Partner Program
    • News Partner Program (英文)
    • Video Partner Program (英文)
    • 安全赏金计划 (英文)
    • Security Research Device Program (英文)
    打开菜单 关闭菜单
    • 与 Apple 会面交流
    • Apple Developer Center
    • App Store 大奖 (英文)
    • Apple 设计大奖
    • Apple Developer Academies (英文)
    • WWDC
    阅读最近新闻。
    获取 Apple Developer App。
    版权所有 © 2026 Apple Inc. 保留所有权利。
    使用条款 隐私政策 协议和准则