Eliminate animation hitches with XCTest
Developer Tools 进阶 22m

使用 XCTest 消除动画卡顿

Eliminate animation hitches with XCTest

2020年6月24日

在 Apple 官方观看视频

一句话判断

Xcode 12 终于把动画性能测试带入了 XCTest 时代,你可以用代码量化和回归检测滚动卡顿、转场掉帧等问题,再也不用靠肉眼判断”好像比上个版本更卡了”。

这场 Session 讲了什么

Apple 在 Xcode 12 中引入了 XCTMetric 系列的新能力,专门用于测量和回归检测 UI 动画性能。过去测试动画流畅度只能靠 Instruments 的 SwiftUI 性能模板或者手动数帧率,现在你可以写出精确的 XCTest 用例来量化每一帧的耗时、帧间隔变化以及是否出现 hitch(掉帧)。

Session 详细拆解了”hitch”这个概念:当一帧的渲染时间超过了 VSync 间隔(通常是 16.67ms),就会产生一个 hitch。Apple 把 hitch 分为两类——frame hitch(单帧渲染过慢)和 commit hitch(主线程在 commit 阶段被阻塞)。XCTMetrics 现在可以精确测量这两种 hitch 的数量和持续时间,还能设置性能基线(baseline),在 CI 中自动检测性能退化。

值得深挖的点

Hitch 时间比率(Hitch Time Ratio)

Apple 引入了一个关键指标:hitch time ratio,即 hitch 总时长除以动画总时长,单位是 ms/s。这个指标比传统的 FPS 更有指导意义。60fps 不等于流畅,因为 60 帧可能其中 5 帧有严重延迟。Hitch time ratio 直接反映用户感知到的卡顿程度。Apple 建议的目标是低于 5ms/s,超过 10ms/s 就属于明显卡顿了。

性能基线与 CI 集成

XCTest 现在允许你为每个测试设置 baseline 值。当测试结果超过 baseline 的容差范围时,测试会失败。这意味着你可以把动画性能测试放入 CI pipeline,每次代码变更都自动检查是否引入了性能退化。这比在 Instruments 里手动跑 profile 要可靠得多——人为观察的不确定性被彻底消除了。

代码片段

测量滚动动画性能

场景:测试 UITableView 滚动时的 hitch 情况。

import XCTest

class ScrollPerformanceTests: XCTestCase {
    func testScrollingPerformance() throws {
        let app = XCUIApplication()
        app.launch()
        
        // 找到列表元素
        let table = app.tables.firstMatch
        
        // 测量滚动动画性能
        let options = XCTMeasureOptions()
        options.invocationOptions = [.manuallyStop]
        
        measure(metrics: [
            XCTClockMetric.utcClock,  // 墙钟时间
            XCTCPUMetric(applicationCPU: true),
            XCTMemoryMetric(applicationMemory: true),
            XCTStorageMetric(applicationStorage: true),
            XCTCPUMetric.totalCPU
        ], options: options) {
            // 执行滚动操作
            table.swipeUp(velocity: .fast)
            table.swipeUp(velocity: .fast)
            table.swipeUp(velocity: .fast)
            stopMeasuring()
            
            // 等待动画完成
            sleep(1)
        }
    }
}

使用 XCTOSSignpostMetric 测量自定义动画

场景:精确测量某个自定义转场动画的帧性能。

func testCustomAnimationPerformance() throws {
    let app = XCUIApplication()
    app.launch()
    
    // 使用 OSSignpost 来标记动画区间
    measure(metrics: [
        XCTOSSignpostMetric.scrollDecelerationMetric  // 系统提供的滚动减速指标
    ]) {
        let table = app.tables.firstMatch
        table.swipeUp()
        
        // 等待滚动完全停止
        let exists = NSPredicate(format: "exists == true")
        expectation(for: exists, evaluatedWith: table)
        waitForExpectations(timeout: 5)
    }
}

设置性能基线检测退化

场景:在 CI 中自动检测动画性能退化。

// 在 Xcode 的 Report Navigator 中设置 baseline 后
// 任何超过 baseline 20% 的偏差都会导致测试失败
// 也可以通过 xcrun 命令行工具设置:
// xcrun xctest export-performance-baseline ...

// 测试代码本身不需要改动
// baseline 配置存储在 .xctestplan 或项目配置中
func testTransitionAnimation() {
    let app = XCUIApplication()
    app.launch()
    
    measure(metrics: [
        XCTOSSignpostMetric.animationPerformanceMetric  // 动画性能指标
    ]) {
        app.buttons["Show Detail"].tap()
        app.navigationBars.buttons.firstMatch.tap()
    }
}

最佳实践

已有项目:优先为用户最常操作的滚动场景(如 feed 列表、商品列表)编写动画性能测试。先用 measure 方法收集几轮数据,建立合理的 baseline 值。不要急于把容差设得太紧,先跑一周 CI 看看数据波动范围,再逐步收紧。

新项目:在搭建 CI 的同时就加入性能测试。把关键路径的动画(启动、Tab 切换、列表滚动)都覆盖上 XCTMetric 测试。与 Session 10076 的 Organizer 数据配合,形成线下测试 + 线上监控的双保险。

还有什么值得关注

  • Xcode 12 新增了 Performance 菜单项,可以直接查看和管理所有性能测试的 baseline。
  • XCTOSSignpostMetric 支持自定义 signpost interval,可以测量任意代码段的性能。
  • 测试结果支持导出为 JSON 格式,方便接入自定义的性能分析面板。
WWDC 2020