大多数浏览器和
Developer App 均支持流媒体播放。
-
了解和消除 app 挂起
探索如何跟踪 app 中的挂起和延迟。我们将向您展示工具和方法,以用于发现挂起及其原因,了解可导致挂起的反面模式,探索消除 GCD 等挂起的最佳实践,并提供有关何时应考虑使用异步代码来提高 app 性能的指导。
资源
相关视频
WWDC23
WWDC22
WWDC21
WWDC20
WWDC19
WWDC17
WWDC16
-
下载
嗨 我叫阿努哈夫 我是OS性能团队的工程师 今天 我很高兴能带您了解 以及如何消除app挂起 本次会话分为四个部分 首先要了解“什么是挂起?” 我们接着会谈论造成挂起的常见原因 以及开发时的注意事项 之后 我们会讨论哪些工具 可以用来监控与诊断挂起 最后 我们会学到消除挂起的 常见策略 以及如何为您的app 选择最适合的一种 我们直接开始吧 先看看我新创的食谱app Desserted 这个应用程序会教您如何 制作我最爱的饮料和甜点 芒果探戈奶昔看起来很好喝 我们点进去 看看制作方法 嗯 好像没有动静 哇 加载的时间比预期还要久 app直接卡住 不接受我任何触控 停滞好几秒
这种体验可以称为“延迟”、“慢” 或是“卡顿” 这些字眼 我或是其他人都不想 用来描述自己的app 在Apple 我们把无响应的 这段时间叫做“挂起”
要了解挂起 以及Desserted 发生了什么事 我们必须先了解app的 主runloop是什么
主runloop是一个循环 应用程序的主线程进到其中 运行事件处理程序 用来响应即将发生的事件 主要是用户互动 用户和app互动时 事件经由runloop接收 处理 最后有必要的话 就会更新UI 以上都发生在runloop的 一次循环中 由主线程运行 每次用户输入都会重复这个流程 这是runloop循环一次时 主线程看起来的样子
如果处理事件耗费很久的时间 延迟就会发生在 从用户输入到任何UI更新之间 更惨的是 在挂起期间 事件被缓冲 主线程也不能处理 如果我在挂起期间和app互动 事件并不会受理 必须等到当下的挂起结束才行 于是挂起就会一个接一个叠加上去
一般来说 超过一秒的延迟 感觉就跟挂起没两样 不过更短的延迟也可能被认为是挂起 比如说 滑动屏幕时 延迟个半秒就让人受不了了 但同样的延迟发生在进入视图时 就没什么感觉
若能消除挂起 您的app就会变得 轻快、迅捷并有所响应
知道什么是挂起后 我们来探讨造成挂起的常见原因
主线程有太多工作在进行时 就会发生挂起 要确定是哪个工作造成的 我们必须看看主线程 在处理事件外还在做什么 这段时间可以拆成两个情况 一种情况是主线程本身工作繁忙 也许是单一长任务 或是许多短任务串连 另一种情况是主线程被别的线程 或系统资源阻挡 我们先来看造成主线程繁忙的 常见原因 积极执行工作是指 工作量超过更新UI所需 使得主线程繁忙的时间更长
在Desserted里 食谱视图 所展示的图像标题 一次只会有四种食材图像 如果主线程要一次加载 所有的食材图像 就要花时间读取、准备 并组合每一张图像 这些工作在进行时 大多不会影响用户看到的画面
视图只会展示四张图像 也只有那四张需要 马上生成出来
另一个挂起成因是 主线程正在执行不相干的工作 请注意 主线程负责处理 主调度队列分派的代码块 也会处理其他队列通过“同步调度” 分派的代码块 当一个队列同步调度到 另一个队列时 原队列中待处理的所有代码块 得先执行 才会轮到新加入的队列 试想 一个app有个低优先级的 串行队列 比方说维护队列 假如主线程将一个代码块 同步调度到maintenanceQueue 它就必须等待该队列所有代码块 执行完成后 才能运行 后来的代码块 主线程执行本身工作的时间 就所剩无几
同样地 如果一个代码块从 别的队列分派到主队列 那个代码块就必须在主线程上执行
就连通过“异步调度”分派的代码块 也是如此
另一个挂起成因是使用次优的API 完成任务有很多方法 务必阅读API文件 才能选择最适合手头任务的API
Desserted为食谱视图中的 所有图像都添加圆角 不过进入视图的时间延迟也增长了
为了添加圆角 Desserted 使用了基于位图的 图形上下文 将图像转换成位图 在位图上套用UIBezierPath后 再将位图转换回图像 这套操作是CPU密集型 会消耗大量内存 也可能会花很久的时间 这是因为我们使用错误的硬件 来执行这个工作 与其使用CPU 我应该要利用GPU
在图层上使用CoreAnimation 方法 绘制圆角 真是轻松又快速 简单用一个例子 说明为手头工作 选错API的情况
探讨了可能造成app主线程忙碌的 常见原因后 我们来调查它 为什么会阻塞
同步API在被调用到返回的期间 会阻挡工作执行 如果API有大量工作 或是有可能阻塞一长段时间的话 就不该在主线程使用 除了延迟外 同步API还会 添加其他故障点
一种案例是在app主线程 向网络发出同步请求的情况 若有5G网络 可能不会有任何延迟 若网速较低 延迟时间可能较久 若信号极差 可能就会无限挂起 无法保证挂起多久才会消除 因此这类同步操作应该尽量避免 在主线程上使用
另一个造成主线程阻塞的原因 是系统资源 因为系统资源常常受到限制 文件I/O是最常使用 最竞争激烈的系统资源 延迟状况端看硬件性能 以及其他同时发生的 读写状况 这些都超出app的掌控 因此 app想要防止挂起 就只能尽可能 避免在主线程使用I/O
数据存储不支持并发 因此特别容易发生问题 如果主线程试图在已经开始写入时 读取数据存储 读取指令会先被推迟 而且在所有 写入器完成以前 可能无限推迟
另一个造成挂起的原因是同步处理 根据定义 同步基元会阻挡执行 所以我们必须要限制 或者小心面对 在主线程使用同步处理
同步的线程可能会花很长的时间 来释放隐式或显式锁 这些是我们要提防的常见基元 包括@synchronized指令 dispatch_sync os_unfair_lock和posix锁 特别要留意信号灯的使用 因为它不会传播优先级 还可能因为抢占而延长挂起 一个常见的反面模式是 在等候信号灯时 试图让异步API变成同步 千万不要在主线程这么做
另一个造成主线程阻塞的原因 是通过执行工作、进程间通信 或是使用系统资源来提取某些 不太会改变的值
Desserted的社交功能有个图标 只有在我把联络人加为朋友后 才会显示出来 每次点进视图 都在所有联络人中 查询哪些人是朋友 是一种方法 但是会添加不必要的成本开销与延迟 因为主线程在框架上阻塞住了 背后正在执行的成本高昂的操作 此外 我所提取的值并不常改变 所以经常查询很不必要 还对系统资源添加负担
CPU、内存、存储等等的 系统资源状态 在发生挂起时影响甚巨 硬件和装置在实地的不同状况 代表真实世界的情境 和临床测试碰到的状况 会有非常显著的差异 您必须尽其所能避免这样的案例 进行鲁棒测试 并使用最旧的支持硬件 作为基准检验 造成挂起的最大原因 就是同时进行太多任务 不论是在主线程上 或是经由主线程调度 为了确保性能 您的app的主线程 必须专注在更新UI的必要任务上
知道造成挂起的常见原因后 我们来谈谈一些有用的工具 帮助您在发展与制造app时 可在app内针对挂起 进行监控与会审
为了要会审挂起 您会想知道 您的app在挂起期间 都在做什么 时间分析工具可以做到这点 它能展示应用程序 在一段时间内的调用堆栈 指明它实际上在执行什么 系统跟踪工具添加更多数据上下文 在系统调用上 虚拟内存错误、I/O 以及进程间和进程内的互动上 更多信息 请参考2016年的谈话 “深探系统跟踪” 接下来 我将使用时间分析器 和系统跟踪工具 来找出造成Desserted挂起的原因
跟踪挂起的轨迹后 这是在工具中 开启的画面 在系统跟踪输出中 红线代表系统调用 紫色峰状图代表虚拟内存错误 水平蓝色条表示主线程工作繁忙 下一步要找出这个工作是什么 这就交给时间分析器 它呈现出调用树状图 是在4.7秒的挂起期间 由主线程的调用堆栈所聚合而成 框起来的这部分树状图 说明了4.6秒的挂起 都是食谱视图内的 loadAllMessages方法所导致的 这个模式似曾相识 Desserted所加载的图像 可能比真正需要的还多
您的应用程序发布后 您可以使用MetricKit 来收集调用树状图 检测实地运用时的挂起次数 这能让您根据用户最常碰到的问题 决定优先修正哪部分 想学习如何使用MetricKit 检测挂起 请参考2020年的谈话 “MetricKit中的新增功能” 我已经发布Desserted 也从MetricKit取得一些挂起报告 我们来看其中一份报告 是否和我们刚刚会审的挂起相似
MetricKit返回的调用树状图 是挂起期间的调用堆栈的聚合 这个树状图的格式 和时间分析器呈现的结果很相似
框起来的部分 显示出不同于 我们用工具所调查的挂起 这是因为我添加了新的社交功能 阻挡了查询联络人的调度队列 没有MetricKit的话 我可能不会发现这个问题 问题仍会存在于成品中 修正挂起时 必须要订定 您的app的性能基线并加以量化 使用Xcode organizer 就能展示性能指标 其中包括一个表格 能展示每个app版本的挂起率 这在会审回归时特别有帮助 请参考这两个谈话 了解更多Xcode Organizer信息
接着 我们来研究一些可以用来 修正app挂起的常见策略
这些策略都能处理多种挂起原因 为了了解哪个修正法最适合您的app 您需要看看它们的副作用与取舍
为了消除并防止挂起发生 请减少 主线程的工作量
有两种做法 第一是优化主线程已进行的工作 以减少运行时间 第二是把工作移开主线程 以不会阻塞的方式 让它能持续响应 我们先看减少主线程执行的做法 从缓存开始 缓存是个好方法 可以快速存取常用资产 或是曾经查询的值 它们通常是内存存储 但也可以存在磁盘中 如果需要跨多种app调用的话 稍后需要用到的格式化资产 像是Desserted内的食材图像标题 就是缓存的好对象 因为每次需要这些资产就创建一次 成本实在太高昂
通过把它们缓存进NSCache 生成资产的开销 就转换成快速的内存读取 这样 我们在工具中看到的挂起 就能被消除了
我们必须有一套准确的 缓存无效机制 才能在 拥有过期数据和频繁更新缓存之间 取得平衡 这个机制应该以异步方式 在次级调度队列中进行 让主线程能持续响应事件
通知观察者是另一个减少 主线程工作量的方法 它让您的app能 对值或状态的改变进行反应 不需要执行成本高昂的按需计算 任何类别都可以发出通知 您自己的也可以 要从特定类别找到通知 可查看它的API文档 要找到所有被观察对象的系统通知 请查阅Apple Developer文档页面 搜索nsnotification.name 适合作为此对象的是 Desserted的社交功能
替以下通知注册观察者 abDatabaseChangedExternally 主线程不再需要等候联络人查询 通知进来后 观察者就会被调用 在这个案例中 它会更新一个缓存值
要让主线程持续响应 这些更新应该设为异步处理 这能通过dispatch_async 把处理程序调度到另一个队列
好 我提供的功能跟以前相同 但没有了我们在MetricKit日志 看到的挂起 另一个消除挂起的方法 是把主线程的工作量移开 首先 我们需要决定移开哪个工作 一般而言 重要任务为UI 提供关键信息 应该留在主线程上 此外 所有视图和视图控制器 都必须在主线程创建、修改与销毁 不过 更新UI元素所需的计算 可以卸载到另一个线程 只留下完成处理程序 在主线程执行实际的更新 在得知计算需要很长时间时 这个模式就很有用 其他较不重要的、维护相关的 或非严格时间要求的任务 应该在别的线程异步执行
这样就能以较低的排程优先级 来运行 比起主线程任务 也能拥有较多的时间来完成 这么做是刻意的 能反映一个观念 主线程应该只负责执行关键性工作
从主线程执行异步操作 最简单明了的方法 就是使用同步API的双胞胎 也就是异步API 我们以网络连接作为例子
通过使用异步NSUAL连接到 同步网络API app就会有响应 异步API通常会以这个字 “Asynchronously”指明 或是在方法名称中 附上完成处理程序
大中央调度(GDC)是一种 强力的多线程机制 您可以用在没有异步API变量的案例 或是想从主线程移开您自己的代码 GCD提供简单的机制 以同步或异步的方式 将任意工作块移到别的线程 这让GCD能非常有效地 消除最普遍造成挂起的原因
要在别的线程异步执行工作块 可将该工作块异步调度到 另一个调度队列
在这个异步的工作块内 可以添加一个完成处理程序 并将它调回主线程上
GCD也能让您预热计算 将一个任务异步调度到一个队列 像是prefetchQueue 该任务将会开始执行 让主线程能自由执行其他工作 当主线程需要结果时 也可以同步调度到prefetchQueue 等待该任务完成 我们只是浅谈了GCD的一些功能 想了解更多 请参考2017年的谈话 “现代化GDC”
我们来了解刚刚说到的解决方案 有哪些地方需要取舍 缓存会使用到内存资源 所以要留意 他们的大小 以避免内存大量成长 还有一点很重要 就是要确保 无效机制是准确的 值才不会过期 通知可能会很吵 使用通知观察者时 务必考虑 通知发布的频率 在处理或合并多个通知前 添加一个滤镜 能减少CPU改动次数 使用异步API时 必须要了解 目前的操作是否应该异步调度 首先要查看它对UI更新 是否有关键性 因为操作系统 会降低异步工作的优先级 使用GCD执行异步任务时 您改变了任务 在您的代码中的执行顺序 必须记住哪个任务的顺序 必须在其他任务之前 以确保您的app不会崩溃 将dispatch_sync和串行队列使用 可以在必要时同步化操作 是很棒的做法 和挂起对用户体验所造成的 严重影响相比 这些取舍都是有值得的
在消除挂起时 请记得 使用Apple框架和API 它们已经和很多种装置兼容 性能强 也时常更新 变得更有效率 也更有效 反复改善您的代码 这样才能有目标性地修正问题 同时能看出各别更改的效果 和系统资源做好邻居 使用超过需求的资源不只会减少 您自己的app性能 也会造成系统内其他操作缓慢 我们一起体验了难以忍受的挂起 了解在您的app中防止挂起有多重要 未来 请通过Xcode Organizer 设定您的app的性能基线 在开发与代码审核期间 请留意可能造成挂起的反面模式 我们已讨论其中七种最常见的 使用时间分析器和系统跟踪工具 来诊断可能出现的问题 使用MetricKit来设定 用户最常碰到的问题优先级 消除您找到的挂起 可通过使用缓存、观察通知 寻找异步替代方案 或是利用大中央调度 遵循这些步骤 您的app将会拥有 更好的性能 提供更好的用户体验 谢谢您的收看 [音乐]
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。