---
title: "WWDC20 10077 - 使用 XCTest 消除动画卡顿"
description: "解读 WWDC20 Session 10077：什么是卡顿（Hitches）、帧渲染为什么会延迟，以及如何用 XCTest 在开发阶段量化并消除动画掉帧。"
date: "2020-06-20"
updated: "2026-10-23"
tags: ["WWDC"]
canonical: "https://vonglo.cn/posts/eliminate-animation-hitches-with-xctest/"
license: "CC BY-NC-SA 4.0"
licenseUrl: "https://creativecommons.org/licenses/by-nc-sa/4.0/"
---
# WWDC20 10077 - 使用 XCTest 消除动画卡顿

> 解读 WWDC20 Session 10077：什么是卡顿（Hitches）、帧渲染为什么会延迟，以及如何用 XCTest 在开发阶段量化并消除动画掉帧。

> 本文基于 [Session 10077 - Eliminate animation hitches with XCTest](https://developer.apple.com/videos/play/wwdc2020/10077/)

## 概述

iOS中动画无处不在，应用中优雅流畅的动画可以显著提升用户体验，同理卡顿也会影响用户体验。通过这个 WWDC Session，我们将学会如何通过 XCTest 来检测滑动和动画过程中的掉帧，进而在开发阶段就能解决问题，避免糟糕的用户体验。

## 如何定义卡顿（Hitches）

当用户在页面上操作时，比如上下滑或者点击导航栏上的返回按钮时，主要焦点集中在手势的交互上。良好的交互体验是提供“众享丝滑”的响应速度，反之用户将会感知到明显的卡顿。我们将这些用户可感知的“抖动”称之为卡顿（Hitches），卡顿是指某一帧画面的显示比预期要晚。卡顿会影响用户体验，甚至让用户失去对应用的兴趣（即卸载🐶）。

![连续帧中第 3 帧停留过久，导致第 4 帧延迟 16.67 毫秒显示](/media/Eliminate-animation-hitches-with-XCTest/Eliminate-animation-hitches-with-XCTest-01.png)

如图所示，我们可以逐帧来看动画过程，当我们缓慢移动时，前2帧按预期显示在屏幕上，滑动很流畅且是“跟手”的，但是第3帧停留在屏幕上的时间超出预期，滑动不再“跟手”，第4帧出现时，列表出现了一次跳变，然后滑动再次变得“跟手”。

### 渲染原理

为了搞清楚为什么会出现上面的状况，我们首先来看看画面是如何展示到屏幕上的。iPhone 或 iPad 上的帧率一般都是60，即刷新频率为60赫兹，每帧耗时16.67毫秒，但是在 iPad Pro 的刷新频率为120赫兹，即每帧耗时8.33毫秒。垂直同步信号决定了是否需要切换当前显示的帧，当某一即将要显示的视频帧在垂直同步信号来临的时候没有显示，就发生了卡顿，卡顿的严重程度取决于那一帧延迟了多久才展示，拿上图为例，第4帧延迟了16.67毫秒才展示到屏幕上。

> 更多介绍推荐参看 [《iOS 保持界面流畅的技巧》](https://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/#2)。

### 量化卡顿

有两种方式可以量化卡顿：

* 活动.卡顿时间：某一帧比预期展示的时间晚了多少毫秒
* 2.卡顿率：总的卡顿时间/总时间

听起来可能比较复杂，这里为什么不是直接用掉帧或者掉帧率来表述呢？因为帧率太绝对，容易造成误解。如果测试过程除了动画外还有其他（闲置）阶段，那么 FPS 的意义就没有那么大，因为闲置期间我们本身就不希望画面会变换，同时一些情况下，我们也会设置一些低于最大 FPS 的帧率，比如一些游戏的帧率是每秒30帧，视频的帧率是每秒24帧。拿iOS系统的**时钟应用**为例，考虑到性能及电池损耗，icon 上的指针的帧率为10。

卡顿时间通常情况下是不可比较的，活动秒内总的卡顿时间和10秒内的总的卡顿时间是没有可比性的。

我们可以通过统一卡顿率（即测试阶段每秒的卡顿时间）来制定一些度量标准来对比不同测试的场景，然后得出大概的用户影响情况。

### 标准推荐

苹果推荐使用以下几个值来定义对用户体验的影响程度：

| 程度  | 标准值 |
|---|---|
| 严重 | 每秒钟卡顿超过10毫秒，严重影响用户体验需要立即解决 |
| 警告 | 每秒钟卡顿在5～10毫秒之间，用户可能会察觉，需要开始介入排查 |
| 良好 | 每秒钟卡顿小于5毫秒，继续保持 |

![卡顿时间比率的 Critical、Warning、Good 三档阈值，分别对应每秒 10 毫秒以上、5 到 10 毫秒和 5 毫秒以内](/media/Eliminate-animation-hitches-with-XCTest/Eliminate-animation-hitches-with-XCTest-02.png)

### 度量及优化

从 iOS14 开始，开发者可以在开发及线上阶段均可使用系统提供的 XCTest 框架来排查动画的卡顿。在单元测试和 UI 测试中可以使用 XCTest 来收集动画相关数据及卡顿信息，同时可以使用 MetricKit 收集线上用户卡顿数据，然后结合 Xcode Organizer 来做性能分析。

这个 Session 主要关注开发阶段如何使用 XCTest 性能测试来捕获卡顿，想要了解如何查看线上用户的卡顿信息可以翻看另外两个 Session。

![XCTest Metrics、MetricKit 与 Xcode Organizer 分别覆盖开发测试、Beta 和公开版本三个阶段](/media/Eliminate-animation-hitches-with-XCTest/Eliminate-animation-hitches-with-XCTest-03.png)

![开发测试、Beta 与公开版本分别对应 Xcode、XCTest Metrics 和 Xcode Organizer，并列出 MetricKit 与 Organizer 两个后续 Session](/media/Eliminate-animation-hitches-with-XCTest/Eliminate-animation-hitches-with-XCTest-04.png)

> [WWDC20 10081-What's New in MetricKit](https://developer.apple.com/videos/play/wwdc2020/10081/) 
> [WWDC20 10076-Diagnose Performance Issues with the Xcode Origanizer](https://developer.apple.com/videos/play/wwdc2020/10076/)

在 Xcode 11 中苹果引入了 XCTMetrics 来帮助开发者衡量一些系统性能数据，比如耗时、CPU 使用率、内存占用、os_signpost 打点以及存储。

![Performance XCTests 中可用的 XCTMetrics 列表，XCTOSSignpostMetric 被高亮标出](/media/Eliminate-animation-hitches-with-XCTest/Eliminate-animation-hitches-with-XCTest-05.png)



Xcode 12 引入了一项独立的启动耗时的衡量标准 `XCTApplicationLaunchMetric`，同时也新增了一些模板供开发者来制定自己的性能衡量标准。

<details>
<summary>👉点击查看新增API</summary>

```objc
@interface XCTOSSignpostMetric (XCTBuiltinOSSignposts)

/*!
 * @property navigationTransitionMetric
 * The XCTMetric object covering navigation transitions between views
 */
@property (readonly, class) id<XCTMetric> navigationTransitionMetric API_AVAILABLE(ios(14.0), tvos(14.0)) API_UNAVAILABLE(macos);

/*!
 * @property customNavigationTransitionMetric
 * The XCTMetric object covering custom navigation transitions between views
 */
@property (readonly, class) id<XCTMetric> customNavigationTransitionMetric API_AVAILABLE(ios(14.0), tvos(14.0)) API_UNAVAILABLE(macos);

/*!
 * @property scrollDecelerationMetric
 * The XCTMetric object covering scroll deceleration animations
 */
@property (readonly, class) id<XCTMetric> scrollDecelerationMetric API_AVAILABLE(ios(14.0), tvos(14.0)) API_UNAVAILABLE(macos);

/*!
 * @property scrollDraggingMetric
 * The XCTMetric object covering scroll dragging animations
 */
@property (readonly, class) id<XCTMetric> scrollDraggingMetric API_AVAILABLE(ios(14.0), tvos(14.0)) API_UNAVAILABLE(macos);

@end

/*!
 * @class XCTApplicationLaunchMetric
 * A metric which measures application launch durations.
*/
API_AVAILABLE(macos(10.15), ios(13.0), tvos(13.0))
__attribute__((objc_subclassing_restricted))
@interface XCTApplicationLaunchMetric : NSObject <XCTMetric>

/*!
 * @method -init
 * Initializes an application launch metric that measures the duration an application takes to display its first frame to screen.
 */
- (instancetype)init;

/*!
 * @method -initWithWaitUntilResponsive
 * Initializes an application launch metric that measures the duration an application takes to display its first frame to screen.
 *
 * @param waitUntilResponsive Specifies the end of the application launch interval to be when the application has displayed the first frame and is responsive.
*/
- (instancetype)initWithWaitUntilResponsive:(BOOL)waitUntilResponsive API_AVAILABLE(macos(10.16), ios(14.0), tvos(14.0));

@end

```

</details>

这里我们主要关注 `XCTOSSignpostMetric`；从 Xcode 11 开始，开发者可以使用 `XCTOSSignpostMetric` 来计算 `os_signpost` 的时间间隔。在 Xcode 12 中，当使用 `os_signpost` 来计算动画时间间隔时，除了可以获取到时间间隔外，还能额外得到以下几个数值：卡顿次数、卡顿总时长、卡顿时间占比、帧率、帧数（其中前三项为 iOS 特有）。

![XCTOSSignpostMetric 在动画时间间隔下额外返回的六项指标：时长、卡顿次数、卡顿总时长、卡顿时间占比、帧率和帧数](/media/Eliminate-animation-hitches-with-XCTest/Eliminate-animation-hitches-with-XCTest-06.png)

![XCTOSSignpostMetric 头文件注释，说明动画间隔下会额外收集帧率、帧数、卡顿次数、卡顿总时长和卡顿时间占比](/media/Eliminate-animation-hitches-with-XCTest/Eliminate-animation-hitches-with-XCTest-07.png)




想要获取这些数据，需要在代码中集成 `os_signpost` 埋点，有3种集成场景

* 计算无动画时间间隔
* 使用 `.animationBegin` 的方式计算动画时间间隔
* UIKit本身的计算方式

> 第一种只返回时间间隔，第二种除了时间间隔以外，还会返回上面提到的几个值。

![创建 os_signpost 时间间隔的三种方式：.begin、.animationBegin 和 UIKit 自带的动画间隔](/media/Eliminate-animation-hitches-with-XCTest/Eliminate-animation-hitches-with-XCTest-08.png)



在 Xcode 11 中，我们只能调用 `os_signpost` 里的 `.begin` 和 `.end` 来测量一些非动画的时间间隔。在 Xcode 12 中，可以指定计算动画时间间隔的方式，仅需将之前的 `.begin` 接口替换为 `.animationBegin`。

```swift
os_signpost(.animationBegin, log: logHandle, name: "performAnimationInterval")
os_signpost(.end, log: logHandle, name: "performAnimationInterval")
```

除了使用自定义的时间间隔，还可以使用系统预置的几个 `UIKit` 相关的 `metric` 来测量导航动画转场以及滑动的场景。

一起来看看如何使用这几个预置的 `metric` 的例子。

```swift
// Measure scrolling animation performance using a Performance XCTest
func testScrollingAnimationPerformance() throws {
    app.launch()
    app.staticTexts["Meal Planner"].tap()
    let foodCollection = app.collectionViews.firstMatch
    
    measure(metrics: [XCTOSSignpostMetric.scrollDecelerationMetric]) {
        foodCollection.swipeUp(velocity: .fast)
    }
}
```

启动应用后，点击 `Meal Planner` 进入文章最开始的那个列表页，然后将向上快速滑动的代码块放到 `measure block` 里，同时指定 `metric` 为 `scrollDecelerationMetric`。

> Xcode 12起，`swipe` 可以指定滑动速度。

`measure block` 里面的代码默认情况下会执行5次来搜集性能数据，这就意味着会连续向上滑动列表5次，即每一次滑动展示的是不同内容，但这不是我们想要的，我们希望5次滑动所处的场景是一致的。可以通过对上面代码做如下两处改造

```swift
func testScrollingAnimationPerformance() throws { 
    app.launch()
    app.staticTexts["Meal Planner"].tap()
    let foodCollection = app.collectionViews.firstMatch

    // 活动
    let measureOptions = XCTMeasureOptions()
    measureOptions.invocationOptions = [.manuallyStop]
        
    measure(metrics: [XCTOSSignpostMetric.scrollDecelerationMetric],
            options: measureOptions) {
        foodCollection.swipeUp(velocity: .fast)
        // 2
        stopMeasuring()
        foodCollection.swipeDown(velocity: .fast)
    }
}
```
活动. 创建 `measureOptions`，指定为手动停止
2. 在 `measure block` 中调用 `stopMeasuring` 方法，然后往下快速滑动重置为初始状态。

测试用例准备就绪，为了消除外部影响，还需要做一些额外设置

* 为性能测试建一个独立的 `scheme`，然后使用 `release` 模式，同时禁用掉调试器。

![Xcode Scheme 的 Test 配置：Build Configuration 设为 Release，并取消勾选 Debug executable](/media/Eliminate-animation-hitches-with-XCTest/Eliminate-animation-hitches-with-XCTest-09.png)

* 禁用掉自动截屏及覆盖率。

![Test Plan 的 Configurations 页：Automatic Screenshots 与 Code Coverage 都设为 Off](/media/Eliminate-animation-hitches-with-XCTest/Eliminate-animation-hitches-with-XCTest-10.png)

* 禁用一些运行时检测功能。

![Diagnostics 页中关闭 Address Sanitizer、Thread Sanitizer、Main Thread Checker 等运行时检测](/media/Eliminate-animation-hitches-with-XCTest/Eliminate-animation-hitches-with-XCTest-11.png)


做完上述操作，即可运行测试用例，然后查看对应结果。在结果页面选中 `Hitch Time Ratio`，可以看到5次运行记录的结果，页面上也会计算出平均值（每秒卡顿1.2毫秒），我们可以设置这个值作为基准值，后续这个用例的结果都会和基准值对比，确保卡顿维持在合理范围，避免将严重卡顿发布到线上，影响用户体验。 

![Performance Result 面板显示 Hitch Time Ratio 五次迭代的测量结果、平均值 1.2 毫秒每秒以及 Set Baseline 按钮](/media/Eliminate-animation-hitches-with-XCTest/Eliminate-animation-hitches-with-XCTest-12.png)

最后，演讲者通过一个简单的 `Demo` 来演示如何使用 `XCTest` 来优化动画中的卡顿，具体示例是在 `cellForItem` 里方法里将图片重绘改成设置视图的 `contentMode`，以此减少主线程卡顿和 CPU 占用。

## 最后

通过以上的演示和新 API 讲解，相信大家对于卡顿的检测和优化有了一个大概的了解，接下来就是亲手实践了。Have Fun～
---

Canonical: https://vonglo.cn/posts/eliminate-animation-hitches-with-xctest/
License: [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/)
Author: Vong
