使用 XCTest 消除动画卡顿
Eliminate animation hitches with XCTest
2020年6月24日
一句话判断
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 格式,方便接入自定义的性能分析面板。