WWDC Quick Look 💓 By SwiftGGTeam
Eliminate animation hitches with XCTest

Eliminate animation hitches with XCTest

观看原视频

Highlight

Apple 在 Xcode 12 中让 XCTOSSignpostMetric 识别动画 os_signpost 区间和 UIKit 滚动、导航子指标,Performance XCTest 可以采集 hitch 数量、总 hitch time、hitch time ratio、frame rate 和 frame count,用测试基线阻止动画回归进入发布流程。


核心内容

列表滚动和页面转场是最容易被低估的性能路径。功能看起来已经完成,用户一滑动才会发现列表没有跟上手指,某一帧停留太久,下一帧又突然跳回正确位置。session 把这种用户能感知到的抖动称为 hitch:一帧晚于预期时间显示在屏幕上。

单看 frames per second 容易误判。一个测试里如果有静止时间,帧率会被拉偏;游戏、视频和时钟图标也可能有意选择低于屏幕刷新率的更新频率。Apple 因此建议用 hitch time 和 hitch ratio 衡量动画问题。hitch time 的目标永远是 0,hitch ratio 则把总 hitch 时间归一化成每秒多少毫秒,便于比较不同长度的测试。

Xcode 12 的改变,是把这套指标带进 XCTest。开发者可以用自定义 animation os_signpost 区间标记动画,也可以直接使用 UIKit 已经标好的滚动和导航区间。测试跑完后,报告 UI 会显示新的动画指标,并允许把当前平均值设成 baseline。下一次改动如果让滚动变慢,Performance XCTest 会在开发流程里先暴露出来。

session 的演示很具体。Meal Planner 应用先用滚动性能测试建立基线,再给列表项加入图片。重新运行测试后,hitch 数量上升,问题定位到主线程上的 scaleAspectFit 调用。修复方式是改用 Core Animation 的 setContentMode,把图片重绘交给 GPU,减少主线程工作量。再次运行测试后,动画指标回到零 hitch。

详细内容

1. 先把 hitch 定义成可比较的指标

00:38)hitch 是一帧比预期更晚显示。iPhone 和 iPad 通常以 60Hz 更新屏幕,每帧约 16.67ms;iPad Pro 可以到 120Hz,每帧约 8.33ms。只要一帧错过了预期的 VSYNC,就会产生 hitch。

02:10)Apple 给出两个量化方式。hitch time 是一帧晚到屏幕上的毫秒数;hitch ratio 是测试期间总 hitch 时间除以测试时长,单位是每秒多少毫秒。session 里给出的经验线是:低于 5ms/s 是好的体验,5 到 10ms/s 需要调查,10ms/s 或更高要立即处理。

2. 用 animation signpost 标记自定义动画区间

06:35XCTOSSignpostMetric 在 Xcode 11 已经能测量 os_signpost interval 的持续时间。Xcode 12 加入 animation interval 后,把开始事件从普通 begin 换成 animationBegin,同一段标记就能返回动画相关指标。

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

关键点:

  • animationBegin 声明这个 interval 对应屏幕动画。
  • end 结束同名 interval,测试只统计这段区间内发生的动画表现。
  • Xcode 12 会在 duration 之外返回 hitch 数量、总 hitch time、hitch time ratio、frame rate 和 frame count。

3. 直接使用 UIKit 已标记的滚动和导航区间

06:55)很多 UIKit 场景不需要自己埋点。XCTOSSignpostMetric 提供了导航转场、滚动减速和滚动拖拽等子指标,适合直接覆盖 App 里最常见的交互路径。

extension XCTOSSignpostMetric {
     open class var navigationTransitionMetric: XCTMetric { get }
     open class var customNavigationTransitionMetric: XCTMetric { get }
     open class var scrollDecelerationMetric: XCTMetric { get }
     open class var scrollDraggingMetric: XCTMetric { get }
}

关键点:

  • navigationTransitionMetric 用来测量标准导航转场。
  • customNavigationTransitionMetric 覆盖自定义导航转场。
  • scrollDecelerationMetric 关注手指离开后的滚动减速阶段。
  • scrollDraggingMetric 关注用户拖拽过程中的滚动表现。

4. 把滚动性能写成 Performance XCTest

07:12)示例测试先启动应用,进入 Meal Planner 页面,再找到第一个 collection view。measure(metrics:) 包住一次快速上滑,XCTest 会收集 scrollDecelerationMetric 对应的动画指标。

// 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)
    }
}

关键点:

  • app.launch() 保证每次测试从应用启动开始。
  • app.staticTexts["Meal Planner"].tap() 进入要测量的页面。
  • app.collectionViews.firstMatch 找到列表容器。
  • measure(metrics:) 指定本次性能测试采集滚动减速指标。
  • swipeUp(velocity: .fast) 使用 Xcode 12 支持的滚动速度参数,减少手势输入的随机性。

5. 每轮测量后重置应用状态

08:02measure 默认会运行五次。直接连续上滑五次,后几轮可能测到不同内容。示例用 XCTMeasureOptions 开启手动停止,在停止测量后再把列表滑回去。

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)
        stopMeasuring()
        foodCollection.swipeDown(velocity: .fast)
    }
}

关键点:

  • XCTMeasureOptions() 创建本次测量的运行选项。
  • .manuallyStop 允许测试代码主动结束指标采集。
  • stopMeasuring() 之后的操作不再进入性能样本。
  • swipeDown(velocity: .fast) 把列表恢复到下一轮可比较的状态。

08:18)测试 scheme 也会影响结果。session 建议为 Performance XCTest 建立单独 scheme,使用 Release build configuration,关闭 debugger、自动截图、code coverage,以及 Runtime Sanitization、Runtime API Checking、Memory Management 里的诊断选项。

09:01)报告 UI 会显示新的动画指标。示例选择 Hitch Time Ratio 后,五轮平均值是 1.2ms/s。开发者可以把这个平均值设成 baseline,让未来测试与它比较。

11:12XCTOSSignpostMetric 只统计 measure block 中发出的目标 signpost interval。一个 measure block 也可以监听多个不同 interval,例如同一次上滑里同时监听 scrollDecelerationMetricscrollDraggingMetric

核心启发

  • 给核心列表加滚动基线:做什么:为首页 feed、商品列表或图库写一个 scrollDecelerationMetric 测试。为什么值得做:这类页面的性能退化通常来自图片、阴影、布局计算或数据绑定,用户最先感知到的是滚动 hitch。怎么开始:选一个固定入口,进入页面后执行 swipeUp(velocity: .fast),把当前稳定结果设成 baseline。

  • 把转场动画纳入 UI 测试:做什么:为关键页面跳转添加 navigationTransitionMetriccustomNavigationTransitionMetric。为什么值得做:导航转场常常夹着数据准备和视图构建,主线程压力会直接变成可见卡顿。怎么开始:用 UI test 触发一次 tap,再让 measure(metrics:) 包住转场动作。

  • 给自定义动画补 signpost:做什么:在自研动画、复杂手势或 canvas 更新代码周围加 animation os_signpost。为什么值得做:UIKit 已覆盖常见滚动和导航,但业务自定义动画需要自己定义测量边界。怎么开始:用 os_signpost(.animationBegin, ...) 和同名 .end 包住动画区间,再在测试里用 XCTOSSignpostMetric 采集结果。

  • 把性能测试的环境单独固定下来:做什么:为 Performance XCTest 建立独立 scheme。为什么值得做:debugger、截图、coverage 和运行时诊断都会干扰测量结果,baseline 会变得不稳定。怎么开始:复制现有测试 scheme,改成 Release,关闭 session 列出的调试和诊断选项。

  • 用测试保护新图片功能上线:做什么:在列表项加入图片、缩略图或动态内容前后跑同一条滚动性能测试。为什么值得做:session 的演示说明,主线程图片缩放会直接增加 hitch。怎么开始:先记录零 hitch 或低 hitch baseline,合入图片功能后比较指标,发现回归时优先检查主线程上的解码、缩放和绘制工作。

关联 Session

评论

GitHub Issues · utterances