本文目录 3 节

Vong WWDC

WWDC20 10077 - 使用 XCTest 消除动画卡顿

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

更新于 2026.10.23 WWDC

本文基于 Session 10077 - Eliminate animation hitches with XCTest

概述

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

如何定义卡顿(Hitches)

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

连续帧中第 3 帧停留过久,导致第 4 帧延迟 16.67 毫秒显示
连续帧中第 3 帧停留过久,导致第 4 帧延迟 16.67 毫秒显示

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

渲染原理

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

更多介绍推荐参看 《iOS 保持界面流畅的技巧》。

量化卡顿

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

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

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

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

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

标准推荐

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

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

卡顿时间比率的 Critical、Warning、Good 三档阈值,分别对应每秒 10 毫秒以上、5 到 10 毫秒和 5 毫秒以内
卡顿时间比率的 Critical、Warning、Good 三档阈值,分别对应每秒 10 毫秒以上、5 到 10 毫秒和 5 毫秒以内

度量及优化

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

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

XCTest Metrics、MetricKit 与 Xcode Organizer 分别覆盖开发测试、Beta 和公开版本三个阶段
XCTest Metrics、MetricKit 与 Xcode Organizer 分别覆盖开发测试、Beta 和公开版本三个阶段

开发测试、Beta 与公开版本分别对应 Xcode、XCTest Metrics 和 Xcode Organizer,并列出 MetricKit 与 Organizer 两个后续 Session
开发测试、Beta 与公开版本分别对应 Xcode、XCTest Metrics 和 Xcode Organizer,并列出 MetricKit 与 Organizer 两个后续 Session

WWDC20 10081-What’s New in MetricKit WWDC20 10076-Diagnose Performance Issues with the Xcode Origanizer

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

Performance XCTests 中可用的 XCTMetrics 列表,XCTOSSignpostMetric 被高亮标出
Performance XCTests 中可用的 XCTMetrics 列表,XCTOSSignpostMetric 被高亮标出

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

👉点击查看新增API
@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

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

XCTOSSignpostMetric 在动画时间间隔下额外返回的六项指标:时长、卡顿次数、卡顿总时长、卡顿时间占比、帧率和帧数
XCTOSSignpostMetric 在动画时间间隔下额外返回的六项指标:时长、卡顿次数、卡顿总时长、卡顿时间占比、帧率和帧数

XCTOSSignpostMetric 头文件注释,说明动画间隔下会额外收集帧率、帧数、卡顿次数、卡顿总时长和卡顿时间占比
XCTOSSignpostMetric 头文件注释,说明动画间隔下会额外收集帧率、帧数、卡顿次数、卡顿总时长和卡顿时间占比

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

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

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

创建 os_signpost 时间间隔的三种方式:.begin、.animationBegin 和 UIKit 自带的动画间隔
创建 os_signpost 时间间隔的三种方式:.begin、.animationBegin 和 UIKit 自带的动画间隔

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

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

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

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

// 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次滑动所处的场景是一致的。可以通过对上面代码做如下两处改造

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
Xcode Scheme 的 Test 配置:Build Configuration 设为 Release,并取消勾选 Debug executable

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

Test Plan 的 Configurations 页:Automatic Screenshots 与 Code Coverage 都设为 Off
Test Plan 的 Configurations 页:Automatic Screenshots 与 Code Coverage 都设为 Off

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

Diagnostics 页中关闭 Address Sanitizer、Thread Sanitizer、Main Thread Checker 等运行时检测
Diagnostics 页中关闭 Address Sanitizer、Thread Sanitizer、Main Thread Checker 等运行时检测

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

Performance Result 面板显示 Hitch Time Ratio 五次迭代的测量结果、平均值 1.2 毫秒每秒以及 Set Baseline 按钮
Performance Result 面板显示 Hitch Time Ratio 五次迭代的测量结果、平均值 1.2 毫秒每秒以及 Set Baseline 按钮

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

最后

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

相关阅读

动态更新

关注微信公众号 Vong 或在微博上关注 @Vong_HUST,第一时间收到新文章。您的支持和鼓励会让这份写作持续下去。