Highlight
Apple 用 Xcode Organizer、MetricKit、Instruments、XCTest 和 App Store Connect API 串起电池、启动、卡顿、内存、磁盘写入、滚动和终止率等性能指标,让开发者能在开发、测试和线上发布后持续发现性能回归。
核心内容
性能问题先出现在用户手里
MealPlanner 加了保存食谱和图片的功能后,问题开始出现。列表滚动会抖,启动时首帧迟迟不出现,保存一次数据可能写入太多文件。开发者在桌面上不一定能复现,但用户已经在真实设备上遇到了这些问题。
过去处理这类问题,常见做法是等用户反馈,再尝试猜测原因。电池、启动、卡顿、内存和磁盘写入各有工具,数据分散,排查路径很长。一个新版本上线后,如果某个指标变差,开发者需要先知道哪里变差,再找到对应代码。
Apple 在这场 session 里给出了一条完整路径。开发中用 Xcode Debug navigator 和 Instruments 观察现场;测试中用 XCTest 写性能测试;线上用 Xcode Organizer、MetricKit 和 App Store Connect API 读取真实设备上的聚合数据。
这条路径解决的是同一个痛点:性能优化不能只靠感觉。每个问题都要有指标,每个指标都要能回到一次用户动作或一段代码。
八个指标对应五套工具
演讲先列出八个核心指标:Battery Usage、Launch Time、Hang Rate、Memory、Disk Writes、Scrolling、Terminations 和 MXSignposts(01:54)。这些指标覆盖了用户最容易感知的体验:耗电、慢启动、界面无响应、滚动卡顿、被系统杀掉。
工具也分层。Xcode Organizer 看线上版本趋势,MetricKit 收集设备端遥测,Instruments 在开发机上定位调用栈,XCTest 把性能要求写成测试,App Store Connect API 让团队把同样的数据接入自己的分析系统。
详细内容
用 MetricKit 收集线上遥测
(05:46)MetricKit 是设备端性能遥测框架。App 只要注册 MXMetricManagerSubscriber,就能接收每日指标和诊断 payload。演讲提到,电池日志、CPU 指标等数据可以和 App 自己的上下文结合,用来缩小线上问题的根因范围。
import MetricKit
class AppMetrics: MXMetricManagerSubscriber {
init() {
let shared = MXMetricManager.shared
shared.add(self)
}
deinit {
let shared = MXMetricManager.shared
shared.remove(self)
}
// Receive daily metrics
func didReceive(_ payloads: [MXMetricPayload]) {
// Process metrics
}
// Receive diagnostics
func didReceive(_ payloads: [MXDiagnosticPayload]) {
// Process metrics
}
}
关键点:
import MetricKit引入 MetricKit API。AppMetrics遵守MXMetricManagerSubscriber,表示这个对象可以接收性能数据。MXMetricManager.shared取得全局 metric manager。shared.add(self)在初始化时注册订阅者。shared.remove(self)在释放时移除订阅者,这是演讲中推荐的做法。didReceive(_ payloads: [MXMetricPayload])接收每日指标 payload。didReceive(_ payloads: [MXDiagnosticPayload])接收诊断 payload。
(07:00)同一批线上数据也会进入 Xcode Organizer。开发者可以查看最近 16 个 App 版本的电池使用趋势,并在 Xcode 13 新增的 Regression pane 里看到最新版本显著上升的指标。
用 XCTest 测量滚动卡顿
(10:29)滚动卡顿来自一个具体事实:新内容没有在下一次屏幕刷新前准备好。用户看到的是列表跳帧、抖动和停顿。演讲用 XCTOSSignpostMetric.scrollDecelerationMetric 写了一个滚动性能测试,防止带卡顿的版本发布。
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)
}
}
关键点:
app.launch()启动 UI 测试目标 App。app.staticTexts["Meal Planner"].tap()进入 Meal Planner 页面。app.collectionViews.firstMatch找到第一个 collection view,作为滚动对象。XCTMeasureOptions()创建测量选项。.manuallyStop让测试手动停止测量,用来排除重置状态的动作。XCTOSSignpostMetric.scrollDecelerationMetric指定测量滚动减速阶段。foodCollection.swipeUp(velocity: .fast)执行快速上滑,这是被测动作。stopMeasuring()停止测量。foodCollection.swipeDown(velocity: .fast)把界面滚回去,为下一次测量恢复状态。
(11:25)iOS 15 和 macOS 12 中,MetricKit 会在问题发生后立即交付诊断,包括 hang diagnostics。线上问题不需要等到第二天的每日 payload,开发者可以更快拿到现场数据。
用 MXSignpost 标记自定义动画
(11:53)滚动之外,App 里还有自定义动画。iOS 15 在 MetricKit 中加入 MXSignpost 相关 API,用来给关键代码区间打点。mxSignpostAnimationIntervalBegin 标记动画开始,mxSignpost 的 .end 标记结束,MetricKit 会收集这段区间里的 hitch-rate telemetry。
import MetricKit
func startAnimating() {
// Mark the beginning of animations
mxSignpostAnimationIntervalBegin(
log: MXMetricManager.makeLogHandle(category: "animation_telemetry"),
name: "custom_animation")
}
func animationDidComplete() {
// Mark the end of the animation to receive the collected hitch rate telemetry
mxSignpost(OSSignpostType.end,
log: MXMetricManager.makeLogHandle(category: "animation_telemetry"),
name: "custom_animation")
}
关键点:
MXMetricManager.makeLogHandle(category:)创建 MetricKit 使用的 log handle。category: "animation_telemetry"给动画遥测分组,便于后续分析。mxSignpostAnimationIntervalBegin标记动画区间开始。name: "custom_animation"给这段动画命名。animationDidComplete()在动画完成时调用。mxSignpost(OSSignpostType.end, ...)标记同名动画区间结束。- 开始和结束使用同一组
log与name,MetricKit 才能把区间对应起来。
用 XCTest 测量磁盘写入
(13:51)频繁写磁盘会拖慢操作,也会影响设备 NAND 健康。演讲建议先用 Instruments 的 File Activity 模板查看文件系统调用,再用 XCTStorageMetric 把关键路径写成性能测试。
// Example performance XCTest
func testSaveMeal() {
let app = XCUIApplication()
let options = XCTMeasureOptions()
options.invocationOptions = [.manuallyStart]
measure(metrics: [XCTStorageMetric(application: app)], options: options) {
app.launch()
startMeasuring()
let firstCell = app.cells.firstMatch
firstCell.buttons["Save meal"].firstMatch.tap()
let savedButton = firstCell.buttons["Saved"].firstMatch
XCTAssertTrue(savedButton.waitForExistence(timeout: 2))
}
}
关键点:
XCUIApplication()创建 UI 测试中的 App 对象。XCTMeasureOptions()创建测量配置。.manuallyStart让测试在 App 启动后再开始测量。XCTStorageMetric(application: app)指定测量这个 App 的磁盘写入。measure(metrics:options:)包住要测量的用户操作。app.launch()启动 App,但此时还没开始测量。startMeasuring()从保存动作前开始记录磁盘写入。firstCell.buttons["Save meal"].firstMatch.tap()模拟用户保存一餐。XCTAssertTrue(savedButton.waitForExistence(timeout: 2))确认保存完成。
(14:16)这个测试可以设置 baseline。超过预期写入量时,测试会失败。线上版本已经发布后,Organizer 的 Disk Writes metric 可以显示当前版本相对旧版本的趋势。超过 1 GB/24 小时的写入还会生成 Disk Writes Reports,并带有 stack trace。
用工具链追踪启动、终止和内存
(19:00)启动时间指用户点击图标到 App 渲染第一帧之间的时间。App Launch 模板会运行 App 五秒,收集 time profile 和 Thread State Trace,帮助开发者看到启动期间线程为什么被阻塞。
func testLaunchPerformance() throws {
let app = XCUIApplication()
measure(metrics: [XCTApplicationLaunchMetric()]) {
app.launch()
}
}
关键点:
XCUIApplication()创建被测 App。measure(metrics:)建立性能测量块。XCTApplicationLaunchMetric()指定测量启动性能。app.launch()是被测动作,对应用户点击图标后的启动流程。
(21:19)内存是 App、系统和内核共享的资源。App 超过内存限制会被系统终止,下一次用户打开时只能走完整启动流程。演讲建议用 Instruments 的 Leaks、Allocations、VM Tracker 模板分析内存,也可以用 MXSignpost 包住关键代码段,收集更细的内存遥测。
import MetricKit
// Collect memory telemetry
func saveAppAssets() {
mxSignpost(OSSignpostType.begin,
log: MXMetricManager.makeLogHandle(category: "memory_telemetry"),
name: "custom_memory")
// save app metadata
mxSignpost(OSSignpostType.end,
log: MXMetricManager.makeLogHandle(category: "memory_telemetry"),
name: "custom_memory")
}
关键点:
mxSignpost(OSSignpostType.begin, ...)标记关键代码段开始。category: "memory_telemetry"把这段遥测归到内存分析类别。name: "custom_memory"给区间命名。// save app metadata代表实际需要观察的资源保存逻辑。mxSignpost(OSSignpostType.end, ...)标记区间结束。- begin 和 end 使用相同的 category 与 name,便于 MetricKit 汇总这一段的内存数据。
核心启发
-
做什么:给 App 做一个性能健康页,只显示最近版本的电池、启动、卡顿和磁盘写入趋势。 为什么值得做:Xcode Organizer 和 App Store Connect API 已经提供线上聚合数据,团队可以把性能回归变成每日可见的指标。 怎么开始:先在 Organizer 手动查看 Battery Usage、Launch Time、Hang Rate 和 Disk Writes,再用 App Store Connect API 拉取同类 JSON 数据接入内部看板。
-
做什么:为首页列表写滚动性能测试。 为什么值得做:滚动卡顿直接影响用户停留时间,
XCTOSSignpostMetric.scrollDecelerationMetric可以把卡顿风险挡在发布前。 怎么开始:用 UI Test 打开列表页,在measure(metrics:)中执行swipeUp(velocity:),用XCTMeasureOptions排除重置状态的步骤。 -
做什么:给图片导入、缓存刷新、批量保存这类重 I/O 操作加磁盘写入测试。 为什么值得做:演讲指出频繁写盘会拖慢体验,Organizer 还会在 24 小时写入超过 1 GB 时生成报告。 怎么开始:用
XCTStorageMetric(application:)包住一次典型保存动作,设置 baseline,让超过预期写入量的代码在测试阶段失败。 -
做什么:为自定义动画和资源保存路径加 MXSignpost。 为什么值得做:MetricKit 可以围绕自定义区间收集 hitch rate 和内存遥测,线上问题能对应到业务动作。 怎么开始:用
MXMetricManager.makeLogHandle(category:)创建分类,在关键路径开始和结束分别调用mxSignpostAnimationIntervalBegin或mxSignpost。 -
做什么:给启动流程建立性能基线。 为什么值得做:启动慢可能导致系统终止,用户下一次打开会重新走完整启动流程。 怎么开始:在 XCTest 中使用
XCTApplicationLaunchMetric()测量启动,再用 Instruments 的 App Launch 模板查看启动前五秒的 time profile 和 Thread State Trace。
关联 Session
- Detect and diagnose memory issues — 继续展开内存指标、memgraph 收集和 XCTest 内存回归测试。
- Understand and eliminate hangs from your app — 深入解释 hang 的成因,并给出用异步代码和 GCD 消除卡顿的做法。
- Analyze HTTP traffic in Instruments — 用 Instruments Network 模板查看 URLSession、task 和 HTTP 请求的网络行为。
- Embrace Expected Failures in XCTest — 管理测试中的已知失败,让性能测试和功能测试更适合长期维护。
评论
GitHub Issues · utterances