飞道的博客

使用 XCTest 消除动画卡顿

485人阅读  评论(0)

点关注,不迷路。每天获取移动端最新进展 ~

作者: Vong,iOS 开发者,目前就职鹅厂

Sessions: https://developer.apple.com/videos/play/wwdc2020/10077/

概述

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

如何定义卡顿(Hitches)

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

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

渲染原理

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

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

量化卡顿

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

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

  • 2.卡顿率:总的卡顿时间/总时间

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

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

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

标准推荐

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

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

度量及优化

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

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

WWDC20 10081-What's New in MetricKit[2]WWDC20 10076-Diagnose Performance Issues with the Xcode Origanizer[3]

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

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


   
  1. @ interface XCTOSSignpostMetric (XCTBuiltinOSSignposts)
  2. /*!
  3.  * @property navigationTransitionMetric
  4.  * The XCTMetric object covering navigation transitions between views
  5.  */
  6. @property (readonly, class) id<XCTMetric> navigationTransitionMetric API_AVAILABLE(ios( 14.0), tvos( 14.0)) API_UNAVAILABLE(macos);
  7. /*!
  8.  * @property customNavigationTransitionMetric
  9.  * The XCTMetric object covering custom navigation transitions between views
  10.  */
  11. @property (readonly, class) id<XCTMetric> customNavigationTransitionMetric API_AVAILABLE(ios( 14.0), tvos( 14.0)) API_UNAVAILABLE(macos);
  12. /*!
  13.  * @property scrollDecelerationMetric
  14.  * The XCTMetric object covering scroll deceleration animations
  15.  */
  16. @property (readonly, class) id<XCTMetric> scrollDecelerationMetric API_AVAILABLE(ios( 14.0), tvos( 14.0)) API_UNAVAILABLE(macos);
  17. /*!
  18.  * @property scrollDraggingMetric
  19.  * The XCTMetric object covering scroll dragging animations
  20.  */
  21. @property (readonly, class) id<XCTMetric> scrollDraggingMetric API_AVAILABLE(ios( 14.0), tvos( 14.0)) API_UNAVAILABLE(macos);
  22. @end
  23. /*!
  24.  * @class XCTApplicationLaunchMetric
  25.  * A metric which measures application launch durations.
  26. */
  27. API_AVAILABLE(macos( 10.15), ios( 13.0), tvos( 13.0))
  28. __attribute__((objc_subclassing_restricted))
  29. @ interface XCTApplicationLaunchMetric : NSObject <XCTMetric>
  30. /*!
  31.  * @method -init
  32.  * Initializes an application launch metric that measures the duration an application takes to display its first frame to screen.
  33.  */
  34. - (instancetype)init;
  35. /*!
  36.  * @method -initWithWaitUntilResponsive
  37.  * Initializes an application launch metric that measures the duration an application takes to display its first frame to screen.
  38.  *
  39.  * @param waitUntilResponsive Specifies the end of the application launch interval to be when the application has displayed the first frame and is responsive.
  40. */
  41. - (instancetype)initWithWaitUntilResponsive:(BOOL)waitUntilResponsive API_AVAILABLE(macos( 10.16), ios( 14.0), tvos( 14.0));
  42. @end

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

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

  • 计算无动画时间间隔

  • 使用 .animationBegin 的方式计算动画时间间隔

  • UIKit本身的计算方式

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

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


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

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

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


   
  1. // Measure scrolling animation performance using a Performance XCTest
  2. func testScrollingAnimationPerformance() throws {
  3.     app.launch()
  4.     app.staticTexts[ "Meal Planner"].tap()
  5.     let foodCollection = app.collectionViews.firstMatch
  6.     
  7.     measure(metrics: [XCTOSSignpostMetric.scrollDecelerationMetric]) {
  8.         foodCollection.swipeUp(velocity: .fast)
  9.     }
  10. }

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

Xcode 12起,swipe 可以指定滑动速度。

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


   
  1. func testScrollingAnimationPerformance() throws { 
  2.     app.launch()
  3.     app.staticTexts[ "Meal Planner"].tap()
  4.     let foodCollection = app.collectionViews.firstMatch
  5.      // 活动
  6.     let measureOptions = XCTMeasureOptions()
  7.     measureOptions.invocationOptions = [.manuallyStop]
  8.         
  9.     measure(metrics: [XCTOSSignpostMetric.scrollDecelerationMetric],
  10.             options: measureOptions) {
  11.         foodCollection.swipeUp(velocity: .fast)
  12.          // 2
  13.         stopMeasuring()
  14.         foodCollection.swipeDown(velocity: .fast)
  15.     }
  16. }

活动. 创建 measureOptions,指定为手动停止 2. 在 measure block 中调用 stopMeasuring 方法,然后往下快速滑动重置为初始状态。

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

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

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

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

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

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

最后

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

关注我们

我们是「老司机技术周报」,每周会发布一份关于 iOS 的周报,也会定期分享一些和 iOS 相关的技术。欢迎关注。

关注有礼,关注【老司机技术周报】,回复「2020」,领取学习大礼包。

支持作者

这篇文章的内容来自于 《WWDC20 内参》。在这里给大家推荐一下这个专栏,专栏目前已经创作了 108 篇文章,只需要 29.9 元。点击【阅读原文】,就可以购买继续阅读 ~

WWDC 内参 系列是由老司机周报、知识小集合以及 SwiftGG 几个技术组织发起的。已经做了几年了,口碑一直不错。 主要是针对每年的 WWDC 的内容,做一次精选,并号召一群一线互联网的 iOS 开发者,结合自己的实际开发经验、苹果文档和视频内容做二次创作。

参考资料

[1]

《iOS 保持界面流畅的技巧》: https://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/#2

[2]

WWDC20 10081-What's New in MetricKit: https://developer.apple.com/videos/play/wwdc2020/10081/

[3]

WWDC20 10076-Diagnose Performance Issues with the Xcode Origanizer: https://developer.apple.com/videos/play/wwdc2020/10076/


转载:https://blog.csdn.net/sinat_35969632/article/details/116214372
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场