大多数浏览器和
Developer App 均支持流媒体播放。
-
揭秘显式构建的模块
探索 Xcode 16 在显式构建的模块方面给构建版本带来了哪些变化。了解如何使用模块来构建代码、显式构建的模块会如何提升编译任务的透明度,以及如何通过跨目标共享模块来优化你的构建版本。
章节
- 0:00 - Introduction
- 0:17 - Agenda
- 0:34 - What are modules?
- 3:57 - Using modules
- 8:37 - Module build log
- 11:28 - Optimize your build
- 14:45 - Wrap up
资源
相关视频
WWDC22
-
下载
大家好 欢迎观看这个视频 今天我将介绍 在 Xcode 中构建 Swift 和 Clang 模块的新方法 这类模块被称为“显式构建模块” 首先 我将概述一下模块的概念 然后 我会说明如何 使用模块来构建代码 接着 我将介绍你会在 新 Xcode 中发现的不同之处 最后 我会介绍如何通过 最大限度地重用 已构建的模块来优化你的构建版本
就本质而言 模块是代码分发单元 用于描述某个库或框架的接口 一个 Swift 目标 包含多个 Swift 文件 这些文件共同代表一个模块 一般来说 单个目标或框架中的所有 Swift 源文件 均属于同一模块 它们所代表的接口会在 Swift 中用访问说明符来显式标记 类以及它的变量会标记为 public 以使它们对导入程序可见
此外 模块还可导入其他模块 从而为整个项目生成一个 非循环模块图 Swift 编译器获取 你编写的外部接口 并将它汇总到只含这个接口的 .swiftinterface 文本文件中
Objective-C 中的模块 表示方法有所不同 不同于 Swift 在 C 系列语言中 模块的接口需手动编写
我们从众所周知的标头概念开始 然后在旁边添加一个 名为“模块映射”的文件 这个文件描述了这些 标头如何构成一个模块 这是 UIKit 模块的一个示例 我们先从 UIKit 的标头开始 然后再添加 UIKit 的模块映射 这个模块映射会告知编译器关于 UIKit 模块的几个不同之处 其一 这是一个框架模块 其二 它的名字是 UIKit @import 需要这个名称 才能正常运行 因为源代码自身不会定义名称
另外 它会指明 UIKit.h 包含这个模块中的所有标头 最后 它会指明 这个模块导入的每个模块 都可由用于导入 UIKit 的代码来使用
每当你的代码包含 针对已命名模块的 import 指令 或针对某个属于模块的标头的 include 指令时 都会使用模块 这是一个名为 ResearchKit 的项目 其中同时包含使用模块的 Swift 和 Objective-C 代码
我们来看看 ResearchKit 中的 Swift 源文件 它首先会导入 SwiftUI 这是一个 Swift 模块 接着它会导入 ResearchKit 模块 这是一个在项目自身内用 Objective-C 实现的 Clang 模块 另一个标签页中 有一个 Objective-C 文件 它包含一些针对项目标头的 import 指令、一个针对 UIKit 的 import 指令 以及一些针对 SDK 标头的 include 指令 Clang 会将这些指令转换为模块 import 指令
此外 模块还允许编译器 在不同源文件之间 共享对接口的解析 这个操作可通过以下步骤来实现 先将每个模块单独编译为 一个二进制文件 以便在 编译项目源时由编译器读取 然后在每次引用这个模块时 导入这个模块的公共接口 在 Swift 中 这个已编译的形式 会表示为 *.swiftmodule 文件 而在 Clang 中则会表示为 *.pcm 或预编译模块文件
但要实现这种重用 首先需找到并编译相应模块 具体该怎么操作呢?
当编译器遇到某个 import 指令时 它会先确定这个 import 指令 针对的是哪个模块 然后获取相应模块的 已编译表示形式 当编译器遇到 @import UIKit 时 它会先在 SDK 中找到 UIKit 的模块映射 在它知道 UIKit 模块是什么后 接着就需要找到 UIKit 对应的 已编译 .pcm 文件 但如果这个文件不存在 该怎么办?
带着这个问题 我们来看看 构建模块的两种高级方法 以及 Xcode 16 有何不同之处 第一种是隐式构建的模块 此时编译器之间会自行相互协调 来管理模块的构建 而 Xcode 的其余部分 并不知道模块的存在 这就是 Swift 和 Clang 自推出以来构建模块的方式 当一个包含 Swift、 C 和 Objective-C 代码的项目 使用隐式构建的模块时 构建过程通常包含多个 长时间运行的任务 每一行均代表 Xcode 可在其中运行构建任务的 单个执行通道 构建系统会启动编译任务 且每个单独的编译器 都会执行隐式发现 并且要么构建一个模块 (如果编译器恰巧最先发现这个模块) 要么是在推进过程中 载入已存在的多个模块
在时间线视图中 这个过程可能如图所示 在构建开始时启动的 编译任务要比临近结束时 启动的任务用时更长
使用隐式构建的模块时 最终的结果可能是一个编译任务 负责隐式构建某个模块 而另一个也需要这个模块 的任务会被阻止 必须先等待这个模块构建完成 这种情况甚至可能会在 构建过程中的多个环节出现 Xcode 16 利用显式 构建的模块改变了这种情况 此时 Xcode 会与编译器 进行协调来发现和构建模块 显式构建的模块会承担 构建模块的隐式工作 并将这项工作提升为 显式构建系统任务
为此 Xcode 会将 每个源文件的编译工作 拆分为三个独立阶段 即 扫描、构建模块 和最终构建原始代码
Xcode 首先会扫描每个源文件 为整个项目构建一个模块图 甚至还会在多个目标之间共享模块 在构建模块图时 它还可开始分派模块编译任务 这类任务就是 构建日志中的显式任务 系统会直接为这些任务提供 它们所依赖的已编译模块 最后一步是执行原始编译任务 这时这些任务已经过修改 以添加它们所依赖的已编译模块
对于显式构建的模块 时间线 视图现在会包含显式扫描任务、 模块编译任务和原始源文件任务 但现在所花的时间已大幅缩短
构建系统知晓模块的一大优点在于 它现在可避免用 尚未准备好运行的任务 来填充执行通道 相反 它会等到所需的模块 均已准备就绪才会运行任务 这样一来 构建系统便可 有效利用可用的执行通道
这个模型构建方法有几个好处 第一个好处是构建会更加可靠 通过将精确的依赖项 和确定性的构建图公开给构建系统 编译器每次都能以同一方式运行 且只需单独再次运行失败的任务 即可重现任意构建失败问题 其他位置不会维护任何隐式状态 这还意味着 干净的构建现在可重新构建模块 这样还能实现更高效的构建 在构建系统完全知晓模块图后 它可做出更为明智的调度选择 而不是让执行通道 被编译任务所阻塞 从而等待模块构建完成 构建系统负责协调模块构建的 另一个优点是 在 Xcode 中调试时 它现在会 将 Swift 模块传递给调试器
使用隐式构建的模块来构建项目时 Xcode 构建和调试器 拥有完全独立的模块图 借助显式构建的模块 调试器现在能够重用已构建的模块 当调试器需了解 Swift 类型时 比如 使用“p”或“po” 来评估表达式时 这个功能可避免再次构建模块 显式构建的模块还会影响 任务在构建日志中的显示方式 在 Xcode 16 中 显式构建的模块可用于所有 C 和 Objective-C 代码 并且可作为预览而针对 Swift 启用 首先 我将为 Swift 启用显式构建的模块 方法是在项目导航器中 选择“ResearchKit”项目
再选择“Build Settings” 然后在筛选框中输入 “explicitly built”
接下来 选择“Explicitly Built Modules”设置并将它设为“Yes”
在为 Clang 和 Swift 启用显式构建的模块后 我就可以开始构建
构建日志将包含大量扫描任务 这些任务只会为项目中的 每个源文件运行一次 并为构建系统生成一个模块导入图 这是一项内置任务 因此不会生成新进程 如此一来 构建系统便可 在当前扫描的源文件之间缓存信息
第二个新任务是编译模块任务 你不会发现这些任务附加于 任何特定项目或目标 实际上 它们属于顶级任务 因为它们可在目标之间共享 对于其中每一个任务 构建系统 都会生成一个单独的编译器进程 这个任务会将指定的模块构建到 某个已编译的模块文件中 它原本是隐式构建的模块中 正常编译期间所执行的工作 现在 这项工作已被拆分出来 构建模块时出现的所有诊断信息 均会附加到这里 而不会附加到恰巧先构建模块 的任意源文件中 你可能会注意到 同一模块会多次进行构建 在这个构建中 UIKit 模块多次出现 这是因为针对不同目标的 某些构建设置 要求构建某一模块的不同变体 这个构建有 2 个 Swift 模块 变体和 4 个 Clang 模块变体
仔细观察这两种变体会发现 Xcode 显示它们具有不同的哈希值 这个哈希值代表构建 这一模块变体所需的 整套命令行参数 这些标志通常为语言标准、 特征宏或 include 路径方面的差异等内容 你可能经常遇到它们 它们也会出现在隐式构建的 模块中 只是更难被发现 因为模块构建并未 在构建系统中公开 编译器会在扫描期间 优化这个参数列表 从而移除那些不影响 模块构建方式的参数 比如未使用的标头搜索路径 现在 我们将返回 ResearchKit 并移除多余的变体 首先 我想收集一些与我的项目 构建的模块相关的额外信息
我会清理 build 文件夹 以重新构建项目中的所有模块
然后使用“Product”>“Perform Action” >“Build with Timing Summary” 来收集有关构建性能的更多信息 采用不兼容构建设置的 不同源文件会生成多个模块变体 例如 C 源文件和 Objective-C 源文件 会以截然不同的方式解析某些模块 多余的模块变体会导致构建过程 不得不执行更多工作
模块变体的部分常见来源包括 其他预处理器宏、 其他语言模式 (比如单个 C 文件与 Objective-C 混合使用) 或语言版本 比如将 Objective-C 与 C17 等较新的 C 版本 搭配使用以及停用自动引用计数 完成构建后 我将前往“filter” 并输入“modules report”
选择“Clang modules report” 会显示存在 2 个 UIKit 变体以及大量其他模块
而选择“Swift module report” 也会显示存在 2 个变体 它们正好解释了我们先前 在构建日志中发现的 4 个不同 UIKit 变体 为了减少变体的数量 我可以检查通常会导致产生 这类变体的构建设置 于是 我在项目导航器中 选择“ResearchKit” 然后在筛选框中输入“macros” 查看是否设置了额外的宏 由于在项目级别不存在这类宏 于是我前往 ResearchKit 目标的构建设置
这个目标有一个其他目标中没有的 额外 ENABLE_FEATURE 宏
通过选择“Levels”模式 我还可同时查看项目级设置
我从目标中移除这个宏并将它 置于 ResearchKit 项目中
接着 再次选择“Product”、“Clean Build Folder” 和“Build with Timing Summary”
这次 编译日志只显示了 3 个 UIKit 变体 一个针对 Objective-C 另两个针对 Swift 一个用于 Clang 模块 另一个用于 Swift 模块
通过统一项目设置 我们可获取这些独立的图 并将它们合并为一个图 通常 你需要在项目中将构建设置 在合理范围内尽可能广泛地移动 无需在目标级别设置语言标准 它应该能上移到项目或工作区级别 这样 模块便会尽可能 在不同源文件之间共享 关于显式构建的模块 我们就介绍到这里
本次讲座的要点包括 显式构建的模块有助于 构建系统控制模块构建 现在 你的构建日志 看起来会有所不同 模块构建中的编译时间 现在会显示为自身的任务 而不会包含在编译任务中 另外 你还可通过 在项目中实现统一的设置来 减少构建的模块变体的数量 从而让那些针对给定源文件 构建的模块可在不同目标之间共享 谢谢观看
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。