用 Xcode 和设备端检测追踪卡顿
Track down hangs with Xcode and on-device detection
2022年6月6日
一句话判断
从开发阶段到发布后,Apple 提供了一整套卡顿(Hang)检测工具链——Xcode 中的 Hangs Instrument、TestFlight 阶段的设备端检测、以及 App Store Connect 中的聚合报告。
这场 Session 讲了什么
性能工具团队的 John Crowson 以一个甜甜圈食品卡车管理应用为例,系统介绍了在不同开发阶段追踪卡顿的工具。卡顿(Hang)被定义为主线程忙碌或等待其他线程/系统资源超过 250 毫秒,导致视图无法更新的时间段。
Session 按照开发流程的三个阶段展开:开发阶段用 Xcode 发现和诊断卡顿、Beta 测试阶段用设备端检测收集数据、发布后通过 App Store Connect 的聚合报告监控。每个阶段都有对应的工具。
值得深挖的点
卡顿的 250ms 阈值定义。Apple 将卡顿定义为主线程超过 250ms 无法响应用户交互。这个阈值是用户体验可感知的起点。持续卡顿会导致用户强制退出应用、转向竞品甚至删除应用留下差评。
开发阶段:Xcode Hangs Instrument。新的 Hangs Instrument 可以在 Instruments 中实时标记卡顿事件,与时间线中的其他追踪数据(CPU、网络、磁盘 IO)关联分析。这让定位卡顿根因变得直观——你可以看到卡顿发生时主线程在做什么。
Beta 阶段:设备端 Hang 检测。TestFlight 和开发签名的应用支持在设备上自动检测卡顿,无需连接 Xcode。检测数据会上报到 App Store Connect,开发者可以在组织层面查看 Beta 测试者的卡顿报告。这对发现开发环境中难以复现的卡顿至关重要。
发布后:App Store Connect Hang 报告。发布后的应用同样会在设备端检测卡顿,聚合数据出现在 App Store Connect 的性能指标中。这是持续监控线上用户体验的关键数据来源。
代码片段
// 常见卡顿模式:在主线程做耗时操作
// 错误示范
func loadDonuts() {
let data = loadDataSyncFromDisk() // 阻塞主线程
donuts = parse(data)
tableView.reloadData()
}
// 正确做法:移到后台线程
func loadDonuts() {
Task.detached {
let data = await loadDataFromDisk()
let parsed = parse(data)
await MainActor.run {
self.donuts = parsed
self.tableView.reloadData()
}
}
}
// 使用 os_signpost 标记关键代码段(配合 Instruments)
import os.signpost
let log = OSLog(subsystem: "com.app.foodtruck", category: "DataLoading")
func loadDonuts() {
os_signpost(.begin, log: log, "Load donuts from API")
// 加载逻辑...
os_signpost(.end, log: log, "Load donuts from API")
}
最佳实践
- 开发阶段就启用 Hangs Instrument 定期检查,不要等到 Beta 测试才发现卡顿
- 250ms 是检测阈值,但目标是让主线程操作在 16ms(60fps)或 8ms(120fps)内完成
- 主线程只做 UI 更新和轻量逻辑,所有耗时操作(IO、网络、计算)都放到后台
- Beta 测试时关注设备端报告,因为真实设备上的性能表现跟模拟器差别很大
- 发布后持续监控 App Store Connect 的 Hang 报告,作为每次版本发布的质量指标
还有什么值得关注
- 参考 “Understand and eliminate hangs from your app”(WWDC21)了解卡顿根因分析方法
- Hangs Instrument 可以与 Time Profiler、System Trace 等其他 Instrument 联合使用
- ProMotion 显示设备(120Hz)对帧时间要求更严,卡顿更容易被用户感知
- App Store Connect 的性能指标还包含启动时间、内存等维度,可以交叉分析