大多数浏览器和
Developer App 均支持流媒体播放。
-
优化 App 大小和运行时性能
了解我们对 Swift 和 Objective-C 运行时进行了哪些优化,以帮助您打造更智能、更快速,而且能够更快启动的 App。探索在使用 Xcode 14 构建 App 以及更新您的部署目标时,如何轻松运行高效的协议检查,发起更小规模的信息发送调用,并对 ARC 进行优化。
资源
相关视频
WWDC22
-
下载
♪ ♪
Ahmed:嗨 我叫 Ahmed 我从事 Clang 和 Swift 编译器的工作 在本次课程中 我们将 深入探讨我们所做的改进 使常见的 Swift 和 Objective-C 操作更快、更高效 以便我们可以改进您的 App 的大小和运行时性能 当您用 Swift 或 Objective-C 编写代码时 总是会与两个主要组件进行交互 首先您使用 Xcode 构建程序 并且使用 Swift 和 Clang 编译器 但是当您运行 App 时 很多繁重的工作都通过 Swift 和 Objective-C 的运行时中完成了 运行时已经嵌入到 我们所有平台的 OS 内 编译器在构建程序时不能做的事情 是由运行时在程序运行的时候完成的 下面来看看我们 在编译器和运行时中所做的几项改进 这个课程与以往不同: 没有新的 API 语言更改或新的构建设置 您不必修改您的代码 因此所有这些改进 对开发人员来说都是透明的 让我们深入了解一下 我们将要介绍四个功能的改进 我们让 Swift 中的协议检查更加高效 我们还减小了 Objective-C 的消息发送调用尺寸 对保留和释放调用做了类似的缩减 最后 我们让 autorelease 省略 变得更快更小 让我们仔细看看内容
让我们从 Swift 中的 协议检查开始
这里我们有一个名为 CustomLoggable 的协议类型 它具有一个只读计算属性 名为 customLogString 我们可以在我们的 log 函数中使用它 该函数对 CustomLoggable 对象做特殊处理 之后 我们将定义一个 包含名称和日期字段的 Event (事件) 类型 我们通过为 customLogString 属性定义 getter 方法 以便于使其符合 CustomLoggable 协议
这让我们能将 Event 的对象传递给 “log” 函数 当我们执行这段代码时 “log” 函数需要检查 我们传递的值是否符合协议 它使用 “as” 运算符实现检查 您可能还见过 “is” 运算符
只要有可能 这个检查行为就会在 编译器进行编译程序时被优化掉 然而 我们并非总能得到足够的信息 所以这通常需要在运行时 借助之前计算的协议检查元数据 进行检查 有了这个元数据 运行时就知道这个特定的对象 是否确实符合协议 然后检查成功
部分元数据是在编译时构建的 但是很多只能在启动时构建 特别是在使用泛型时 当您使用大量协议 (Protocol) 类型时 这可能会增加数百毫秒 在现实世界的 App 中 我们看到这占用了高达一半的程序启动时间 现在使用新版 Swift 编译环境 我们可以提前对这些进行预计算 在启动时作为 app 可执行文件 和任何用到的 dylib 库 的 dyld 闭包的一部分 最重要的是 此功能也会为现有的 App 启用 只要运行在 iOS 16、tvOS 16 或 watchOS 9 平台上 如果您想了解有关 dyld 和启动闭包的更多信息 请观看 “App 启动时间: 过去、现在和未来” 的讲座 这是 Swift 中的协议 (protocol) 检查
让我们再看看消息发送 使用 Xcode 14 中的 新编译器和链接器 我们缩短了消息发送调用的字节数 在 ARM64 上 从 12 字节减少了 8 字节 正如我们稍后会看到的 消息发送真的无处不在 所以这些优化会累加 我们在二进制文件上 看到了高达 2% 的代码大小改进 这个功能在使用 Xcode 14 构建程序时会自动启用 哪怕您以较老的 OS 作为部署目标 它默认会平衡应用大小和程序性能 但您可以选择使用 objc_stubs_small 链接器标识 仅针对尺寸进行优化 现在让我们看看 到底发生了什么变化 从一个例子开始 在这里 我们想为会议的 开始日期建立一个 NSDate 对象 我们首先生成一个 NSCalendar 对象 然后我们填写 NSDateComponents 的内容 作为会议日程最后返回数值 现在让我们看看编译器生成的结果 现在 汇编的细节并不是非常重要 我们编译器团队的人整天盯着它 是否有问题 所以您就不必再这么做了 重要的是这里几乎每一行 最终都需要一条指令来调用 objc_msgSend 即使我们在执行属性访问时 对日期组件所做的也是如此 这是因为在编译时 我们不知道调用哪个方法 只有 objc 运行时知道 所以我们在运行时中使用 objc_msgSend 方法 并要求它找到正确的方法 让我们关注其中一个调用 我们已经提到了调用 objc_msgSend 的指令 但还有更多的指令 为了告诉运行时调用哪个方法 我们必须将 selector 传递给这些 objc_msgSend 以供调用 这需要更多的指令 来准备 selector 当我们查看二进制文件时 会发现每一个指令都会占用一些空间 在 ARM64 上 每个指令 4 字节 因此 对于每个 objc_msgSend 的调用 我们都会使用 12 字节 并且需要在 每一次调用的时候使用它 这样空间占用就真的越来越大 让我们来看看怎么改进它
现在 正如我们之前所见 其中 8 个字节专用于准备 selector 有趣的是 对于任何给定的 selector 它的代码都是相同的 这就是我们可以优化的地方 由于这始终是相同的代码 因此我们可以共享这段代码 让每个 selector 只发出一次消息 而不是每次发送消息时 都要发送一次 我们可以把它拿出来 放到一个小型 helper 函数中 并改为调用该函数 在使用相同 selector 的 多次调用中 我们可以保存所有这些指令字节 我们将此 helper 函数称为 “selector stub” (选择器存根)
不过 我们仍然需要调用 真正的 objc_msgSend 函数 所以我们继续 再来看 它要加载另一个不同且间接的 函数本身的地址并调用它 细节不重要 重要的是我们需要 另外再用几个字节的代码 才能做到这一点
在这里您可以选择您想要的模式 正如我之前提到的 我们可以将这两个小 stub 函数分开 就像我们在这里所做的那样 我们可以最大限度的共享代码 并让这些方法尽可能的小 但不幸的是 这会连续做两个调用 这对于性能来说并不理想 因此 我们可以使用 替代版本改进这个问题 我们可以将这两个 stub 函数 合并为一个 这样 我们就可以让代码更紧凑 且不需要那么多调用 改进版本就在屏幕右边
所以这有两个选项 您可以选择 是否仅针对尺寸进行优化 最大程度的节省尺寸 您可以使用 -objc_stubs_small 链接器标识启用这个功能 或者您可以使用 同时能保持程序最佳性能和 优化程序大小的代码生成 除非您有严重的尺寸限制 否则我们建议您使用它 这就是为什么 这个功能是默认启动的 这就是使用 stub 让消息发送更小 我们所做的另一项改进 是使保留和释放的开销更小 使用 Xcode 14 中的 新编译器 使用保留和释放的调用 现在在 ARM64 上 从 8 个字节下降了 4 个字节 正如我们稍后会看到的 就像消息发送一样 保留和释放也无处不在 所以这加起来 我们在二进制文件中 看到了代码大小有 2% 的提高改进 现在 与消息发送存根 (stub) 不同 保留和释放确实需要运行时的支持 因此您会自动得到这个改进 当您将部署目标迁移到 iOS 16、tvOS 16 或 watchOS 9 平台的时候 现在让我们看看发生了什么变化 让我们回到示例中来 我们谈到了 msgSend 调用 但是如果使用自动引用计数或 ARC 编译器依旧会插入大量 保留和释放的调用 在非常高的层次上 每当我们复制一个指向对象的指针时 我们都需要增加它的 保留计数来维持其生命周期 在这里 这种情况出现在 我们的指针变量 cal、dateComponent 和 theDate 上 我们通过在运行时 调用 objc_retain 来做到这一点 当超出变量作用域 我们就要调用 objc_release 来减少保留计数 当然 ARC 的好处之一 是其所拥有的编译器魔法 它消除了很多这样的调用 以这些调用保持在最低限度 稍后我们将讨论这些魔法中的一个 但即使有这么多魔法 我们仍然经常需要这些功能调用 在这个例子中 我们最终需要发布日历应用 和 dateComponents 的本地副本
在底层 这些 objc_retain 或 objc_release 函数 只是普通的 C 语言函数 它只接受一个参数 即要释放的对象 因此 使用 ARC 编译器会插入对这些 C 语言函数的调用 并传递适当的对象指针 基于此 这些调用 必须遵守 C 语言调用的约定 并由我们平台的 应用二进制接口即 ABI 进行定义 具体来说 这意味着 我们需要更多的代码执行调用 以便指针传递到正确的寄存器中 所以进而导致我们为此需要 一些额外的 “move” 指令 这就是我们新优化的切入点 通过自定义调用约定 定制保留和释放 我们可以根据对象指针已在的位置 适时的使用正确的变体 而不需要移动它 具体来说 这意味着 我们摆脱了所有这些调用的 一堆冗余代码 再说一次 虽然看起来这些是 微不足道的小指令 带来的改变似乎并不多 但在整个 App 中 它的效果是确实累加的 这就是我们降低 保留和释放操作成本的方式 最后 我们来谈谈autorelease 省略 这个功能就更有趣了 通过 objc 运行时的修改 我们使 autorelease 省略的速度更快 对于现有的 App 在新版 OS 上 运行时会自动生效 在此基础上 通过额外的编译器更改 我们还能使二进制代码变得更小 您将自动获得这种程序尺寸的优势 只要将部署目标迁移到 iOS 16、tvOS 16 或 watchOS 9
所有特性都非常棒 但首先什么是 autorelease 省略 让我们回到示例中来 我之前提到 ARC 已经提供了许多“编译器魔法” 用来优化保留和释放功能 因此 让我们关注这样的案例 autorelease 返回值 在这个例子中 我们生成了一个临时对象 并将它返回给调用方 那么看看它是如何工作的 我们有了临时变量 theDate 我们返回它 完成调用 然后调用方 将其保存到自己的变量中 那么让我们看看 它是如何与 ARC 一起工作的 ARC 在调用方中插入保留方法 在被调用函数中插入释放方法 这里 当我们返回我们的临时对象时 我们需要在函数中首先释放它 因为变量离开了作用域 但我们现在还不能这样做 因为还没有其他变量引用它的值 如果我们真的释放了它 它就会在我们拿到返回值之前被销毁 这不是我们要的 所以有一个特殊的约定 用来返回临时值 我们在返回前为它做 autorelease 以便调用方能够保留它 您之前可能已经见过 autorelease 和 autoreleasepool 这只是一种将释放动作 推迟到以后某个时间点的方法 运行时并不能保证释放的时间 但只要不是此时此刻就可以 因为它允许我们 返回刚才说的临时对象 但这不是毫无开销的 autolrease 有一些开销 这就是 autorelease 省略的用武之地 要了解它是如何工作的 让我们看一下汇编并追溯返回变量 当我们调用 autorelease 时 它会进入 objc 运行时 这就是乐趣开始的地方 运行时试图识别出 正在发生的事情 我们正在返回一个 autoreleased 的值 为了帮助它 编译器发出了一个特殊的标记 我们在其他地方从不使用这个标记 它会告诉运行时 这符合 autoreleased 省略的条件 然后是保留操作 我们稍后会执行它 但是现在 我们还在 autorelease 过程中 当我们这样做时 运行时会加载这个标记指令作为数据 并判断它是否是期望的特殊标记值 如果是 意味着编译器告诉运行时 我们正在返回一个 将立即保留的临时对象 这就可以让我们省略或删除 匹配的 autorelease 和保留调用 这就是 autorelease 省略
但是 这也不是毫无开销的 将代码作为数据加载 并不是很常见的事情 因为它在 CPU 的性能上 不是最优的 我们可以做得更好一点 所以让我们再次追溯返回序列 这次使用新的方式 我们从 autorelease 开始 这仍然会进入 Objective-C 运行时 到这里 我们其实已经有了 有价值的信息:返回的地址 它告诉我们在这个函数完成执行后 我们需要返回到哪里 所以我们可以跟踪它 值得庆幸的是 获得返回地址的开销非常小 它只是一个指针 我们只需要一点资源就可以保存它了 然后我们离开运行时 autorelease 的调用 回到调用方 并在进行保留操作时 重新进入运行时 这就是新的魔法发生的地方 在代码的这一行 可以看到我们现在处于什么地址 并获得一个指向 我们当前返回地址的指针 在运行时里 我们可以比较刚刚得到的 做保留的这个指针与之前保留的 做 autorelease 时的指针 因为我们只是比较两个指针 这个开销非常低 我们不需要进行大开销的内存访问 如果比较成功那我们就知道 可以省略 autorelease/retain 的成对操作了 这样我们就可以提高一些性能
最重要的是现在我们不再需要将 这个特殊的标记指令作为数据 因此我们可以删除它 这也让我们节省了一些代码大小 这就是我们如何让 autorelease 省略变得更快更小 我们介绍了几项 Swift 和 Objective-C 运行时的改进 现在让我们结束吧 当您的 App 在新 OS 上运行时 由于运行时的改进 Swift 协议检查更有效 每次我们尝试进行 autorelease 省略时 速度也会更快 感谢 Xcode 14 中的 新编译器和链接器以及消息发送 stub 通过重新构建 App 您最多可以将代码大小缩小 2% 最后 当您将部署目标平台更新到 iOS 16、tvOS 16 或者 watchOS 9 您可以通过保留和释放功能的调用更小 再节省 2% 的代码空间 更要感谢更小的 autorelease 省略序列 希望您喜欢这次对 Swift 和 Objective-C 编译环境的深入讲解 感谢收看
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。