关于 metrickit-diagnostics
面向 Apple 平台应用开发者的 MetricKit 诊断技能,说明如何收集性能指标与崩溃等诊断数据,并将数据持久化、分析或上传。
主要功能
- 01订阅并接收指标与诊断负载
- 02解析启动、CPU、内存和网络指标
- 03提取调用栈并用于符号化
- 04采集自定义 Signpost 指标
- 05持久化及上传 JSON 负载
- 06测量扩展启动任务
适用场景
- 01排查线上应用的崩溃与卡顿
- 02分析版本间的性能变化
- 03将设备指标发送到自有分析后端
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill metrickit-diagnostics安装后在 Claude Code 中输入/metrickit-diagnostics确认已加载。
详细介绍
MetricKit 诊断
使用 MetricKit 从生产设备收集汇总性能指标和崩溃诊断信息。该框架每天会提供一次指标载荷(CPU、内存、启动时间、卡顿率、动画卡顿、网络用量),并立即提供诊断载荷(崩溃、卡顿、磁盘写入异常),其中包含完整的调用栈树,便于排查问题。
目录
订阅者设置
尽早注册订阅者,最好在
application(_:didFinishLaunchingWithOptions:) 或 App.init 中注册。首次访问 MXMetricManager.shared 后,MetricKit
便会开始累积报告。
import MetricKit
final class MetricsSubscriber: NSObject, MXMetricManagerSubscriber {
static let shared = MetricsSubscriber()
func subscribe() {
MXMetricManager.shared.add(self)
}
func unsubscribe() {
MXMetricManager.shared.remove(self)
}
func didReceive(_ payloads: [MXMetricPayload]) {
// Handle daily metrics
}
func didReceive(_ payloads: [MXDiagnosticPayload]) {
// Handle diagnostics (crashes, hangs, disk writes)
}
}
UIKit 注册
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
MetricsSubscriber.shared.subscribe()
return true
}
SwiftUI 注册
@main
struct MyApp: App {
init() {
MetricsSubscriber.shared.subscribe()
}
var body: some Scene {
WindowGroup { ContentView() }
}
}
接收指标载荷
MXMetricPayload 大约每 24 小时送达一次,其中包含汇总指标。如果之前有载荷未送达,数组中可能会包含多个载荷。
func didReceive(_ payloads: [MXMetricPayload]) {
for payload in payloads {
let begin = payload.timeStampBegin
let end = payload.timeStampEnd
let version = payload.latestApplicationVersion
// Persist raw JSON before processing
let jsonData = payload.jsonRepresentation()
persistPayload(jsonData, from: begin, to: end)
processMetrics(payload)
}
}
可用性:MXMetricPayload — iOS 13.0+、macOS 10.15+、visionOS 1.0+
接收诊断载荷
MXDiagnosticPayload 会提供崩溃、卡顿、CPU 异常、磁盘写入和应用启动诊断信息。在 iOS 15+ 和 macOS 12+ 中,诊断信息会立即送达,而不是与每日报告打包送达。
func didReceive(_ payloads: [MXDiagnosticPayload]) {
for payload in payloads {
let jsonData = payload.jsonRepresentation()
persistPayload(jsonData)
if let crashes = payload.crashDiagnostics {
for crash in crashes {
handleCrash(crash)
}
}
if let hangs = payload.hangDiagnostics {
for hang in hangs {
handleHang(hang)
}
}
if let diskWrites = payload.diskWriteExceptionDiagnostics {
for diskWrite in diskWrites {
handleDiskWrite(diskWrite)
}
}
if let cpuExceptions = payload.cpuExceptionDiagnostics {
for cpuException in cpuExceptions {
handleCPUException(cpuException)
}
}
if let launchDiags = payload.appLaunchDiagnostics {
for launchDiag in launchDiags {
handleSlowLaunch(launchDiag)
}
}
}
}
可用性:MXDiagnosticPayload — iOS 14.0+、macOS 12.0+、visionOS 1.0+
关键指标
启动时间 — MXAppLaunchMetric
if let launch = payload.applicationLaunchMetrics {
let firstDraw = launch.histogrammedTimeToFirstDraw
let optimized = launch.histogrammedOptimizedTimeToFirstDraw
let resume = launch.histogrammedApplicationResumeTime
let extended = launch.histogrammedExtendedLaunch
}
运行时间 — MXAppRunTimeMetric
if let runTime = payload.applicationTimeMetrics {
let fg = runTime.cumulativeForegroundTime // Measurement<UnitDuration>
let bg = runTime.cumulativeBackgroundTime
let bgAudio = runTime.cumulativeBackgroundAudioTime
let bgLocation = runTime.cumulativeBackgroundLocationTime
}
CPU、内存和响应能力
if let cpu = payload.cpuMetrics {
let cpuTime = cpu.cumulativeCPUTime // Measurement<UnitDuration>
}
if let memory = payload.memoryMetrics {
let peakMemory = memory.peakMemoryUsage // Measurement<UnitInformationStorage>
}
if let responsiveness = payload.applicationResponsivenessMetrics {
let hangTime = responsiveness.histogrammedApplicationHangTime
}
if let animation = payload.animationMetrics {
let scrollHitchRate = animation.scrollHitchTimeRatio // Measurement<Unit>
}
网络和蜂窝网络
if let network = payload.networkTransferMetrics {
let wifiUp = network.cumulativeWifiUpload // Measurement<UnitInformationStorage>
let wifiDown = network.cumulativeWifiDownload
let cellUp = network.cumulativeCellularUpload
let cellDown = network.cumulativeCellularDownload
}
应用退出指标
if let exits = payload.applicationExitMetrics {
let fg = exits.foregroundExitData
let bg = exits.backgroundExitData
// Inspect normal, abnormal, watchdog, memory, etc.
}
调用栈树
每个诊断信息(崩溃、卡顿、CPU 异常、磁盘写入、应用启动)都附带一个 MXCallStackTree。使用 jsonRepresentation() 提取并进行符号化。
func handleCrash(_ crash: MXCrashDiagnostic) {
let tree = crash.callStackTree
let treeJSON = tree.jsonRepresentation()
let exceptionType = crash.exceptionType
let signal = crash.signal
let reason = crash.terminationReason
uploadDiagnostic(
type: "crash",
exceptionType: exceptionType,
signal: signal,
reason: reason,
callStack: treeJSON
)
}
func handleHang(_ hang: MXHangDiagnostic) {
let tree = hang.callStackTree
let duration = hang.hangDuration // Measurement<UnitDuration>
uploadDiagnostic(type: "hang", duration: duration, callStack: tree.jsonRepresentation())
}
JSON 结构包含一个调用栈帧数组,其中包含二进制文件名称、偏移量和地址。使用 atos 进行符号化,或将 dSYM 上传到分析服务。
可用性:MXCallStackTree — iOS 14.0+、macOS 12.0+、visionOS 1.0+
自定义 Signpost 指标
使用带有 MetricKit 日志句柄的 mxSignpost 来采集自定义性能区间。这些指标会显示在每日的 MXMetricPayload 中,位于 signpostMetrics 下。
创建日志句柄
let metricLog = MXMetricManager.makeLogHandle(category: "Networking")
发出 Signpost
import os
func fetchData() async throws -> Data {
let signpostID = MXSignpostIntervalData.makeSignpostID(log: metricLog)
mxSignpost(.begin, log: metricLog, name: "DataFetch", signpostID: signpostID)
let data = try await URLSession.shared.data(from: url).0
mxSignpost(.end, log: metricLog, name: "DataFetch", signpostID: signpostID)
return data
}
从载荷读取自定义指标
if let signposts = payload.signpostMetrics {
for metric in signposts {
let name = metric.signpostName // "DataFetch"
let category = metric.signpostCategory // "Networking"
let count = metric.totalCount
if let intervalData = metric.signpostIntervalData {
let avgMemory = intervalData.averageMemory
let cumulativeCPUTime = intervalData.cumulativeCPUTime
}
}
}
系统会限制每个日志中的自定义 Signpost 指标数量,以降低设备上的开销。请将自定义指标保留给关键代码路径。
导出和上传载荷
两种载荷类型都遵循 NSSecureCoding,并提供 jsonRepresentation() 以便轻松序列化。
func persistPayload(_ jsonData: Data, from: Date? = nil, to: Date? = nil) {
let fileName = "metrics_\(ISO8601DateFormatter().string(from: Date())).json"
let url = FileManager.default.temporaryDirectory.appending(path: fileName)
try? jsonData.write(to: url)
}
func uploadPayloads(_ jsonData: Data) {
Task.detached(priority: .utility) {
var request = URLRequest(url: URL(string: "https://api.example.com/metrics")!)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = jsonData
_ = try? await URLSession.shared.data(for: request)
}
}
获取过去的载荷
如果载荷送达时订阅者尚未注册,可使用 pastPayloads 和 pastDiagnosticPayloads 获取载荷。这些属性会返回自上次分配共享管理器以来生成的报告。
let pastMetrics = MXMetricManager.shared.pastPayloads
let pastDiags = MXMetricManager.shared.pastDiagnosticPayloads
扩展启动测量
使用扩展启动测量,将首帧绘制后的设置工作(加载数据库、恢复状态)纳入启动指标。
let taskID = MXLaunchTaskID("com.example.app.loadDatabase")
MXMetricManager.shared.extendLaunchMeasurement(forTaskID: taskID)
// Perform extended launch work...
await database.load()
MXMetricManager.shared.finishExtendedLaunchMeasurement(forTaskID: taskID)
扩展启动时间会显示在 MXAppLaunchMetric 的 histogrammedExtendedLaunch 下。
Xcode Organizer 集成
Xcode Organizer 会显示所有已选择共享诊断信息的用户的汇总 MetricKit 数据。使用 Organizer 进行趋势分析:
- Metrics 标签页:随时间变化的电池、性能和磁盘写入指标
- Regressions 标签页:按版本自动检测指标回归
- Crashes 标签页:包含符号化堆栈跟踪的崩溃日志
设备端的 MetricKit 收集功能可与 Organizer 互为补充,让你能够将原始数据发送到自己的后端,用于自定义仪表板、警报和按用户群体筛选。
常见错误
不要:过晚订阅 MXMetricManager
系统可能会在启动后不久交付待处理的有效负载。过晚订阅(例如,在视图控制器中订阅)可能会导致完全错过这些数据。
// WRONG — subscribing in a view controller
override func viewDidLoad() {
super.viewDidLoad()
MXMetricManager.shared.add(self)
}
// CORRECT — subscribe in application(_:didFinishLaunchingWithOptions:)
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions opts: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
MXMetricManager.shared.add(metricsSubscriber)
return true
}
不要:忽略 MXDiagnosticPayload
只处理 MXMetricPayload 会导致你错过崩溃、卡顿和磁盘写入诊断信息——这些是 MetricKit 提供的最具操作价值的数据。
// WRONG — only implementing metric callback
func didReceive(_ payloads: [MXMetricPayload]) { /* ... */ }
// CORRECT — implement both callbacks
func didReceive(_ payloads: [MXMetricPayload]) { /* ... */ }
func didReceive(_ payloads: [MXDiagnosticPayload]) { /* ... */ }
不要:未先持久化就处理有效负载
系统只会交付每个有效负载一次。如果订阅者在处理过程中崩溃,数据将永久丢失。
// WRONG — process inline, crash loses data
func didReceive(_ payloads: [MXDiagnosticPayload]) {
for p in payloads {
riskyProcessing(p) // If this crashes, payload is gone
}
}
// CORRECT — persist raw JSON first, then process
func didReceive(_ payloads: [MXDiagnosticPayload]) {
for p in payloads {
let json = p.jsonRepresentation()
try? json.write(to: localCacheURL()) // Safe on disk
Task.detached { self.processAsync(json) }
}
}
不要:在 didReceive 中同步执行繁重工作
回调会在任意线程上运行。繁重处理或同步网络调用会阻塞回调,延迟后续有效负载的交付。
// WRONG — synchronous upload in callback
func didReceive(_ payloads: [MXMetricPayload]) {
for p in payloads {
let data = p.jsonRepresentation()
URLSession.shared.uploadTask(with: request, from: data).resume() // sync wait
}
}
// CORRECT — persist and dispatch async
func didReceive(_ payloads: [MXMetricPayload]) {
for p in payloads {
let json = p.jsonRepresentation()
persistLocally(json)
Task.detached(priority: .utility) {
await self.uploadToBackend(json)
}
}
}
不要:期望在开发过程中立即获得数据
MetricKit 会按 24 小时窗口汇总数据。添加监测代码后不会立即收到有效负载。开发期间可使用 Xcode Organizer 或模拟有效负载,加快迭代。
审查清单
- 在
application(_:didFinishLaunchingWithOptions:)或App.init中调用MXMetricManager.shared.add(subscriber) - 订阅者遵循
MXMetricManagerSubscriber并继承NSObject - 实现
didReceive(_: [MXMetricPayload])和didReceive(_: [MXDiagnosticPayload]) - 处理前将原始
jsonRepresentation()持久化到磁盘 - 将繁重处理异步分派到回调线程之外
- 上传
MXCallStackTreeJSON 和 dSYM,以便进行符号化 - 将自定义 signpost 指标限制在关键代码路径中
- 启动时检查
pastPayloads和pastDiagnosticPayloads,确认是否有遗漏的交付 - 扩展启动任务同时调用
extendLaunchMeasurement和finishExtendedLaunchMeasurement - 分析后端能够接受并存储 MetricKit JSON 格式
- 查看 Xcode Organizer 中的回归趋势,并与设备端数据对照