metrickit-diagnostics by dpearson2699/swift-ios-skills
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill metrickit-diagnostics使用 MetricKit 从生产设备收集聚合性能指标和崩溃诊断信息。该框架提供每日指标数据(CPU、内存、启动时间、卡顿率、动画卡顿、网络使用情况)和即时诊断数据(崩溃、卡顿、磁盘写入异常),并附带完整的调用堆栈树用于问题排查。
尽可能早地注册订阅者——理想情况下在 application(_:didFinishLaunchingWithOptions:) 或 App.init 中。MetricKit 在首次访问 MXMetricManager.shared 后开始累积报告。
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]) {
// 处理每日指标
}
func didReceive(_ payloads: [MXDiagnosticPayload]) {
// 处理诊断信息(崩溃、卡顿、磁盘写入)
}
}
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
MetricsSubscriber.shared.subscribe()
return true
}
@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
// 在处理前持久化原始 JSON
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+
if let launch = payload.applicationLaunchMetrics {
let firstDraw = launch.histogrammedTimeToFirstDraw
let optimized = launch.histogrammedOptimizedTimeToFirstDraw
let resume = launch.histogrammedApplicationResumeTime
let extended = launch.histogrammedExtendedLaunch
}
if let runTime = payload.applicationTimeMetrics {
let fg = runTime.cumulativeForegroundTime // Measurement<UnitDuration>
let bg = runTime.cumulativeBackgroundTime
let bgAudio = runTime.cumulativeBackgroundAudioTime
let bgLocation = runTime.cumulativeBackgroundLocationTime
}
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
// 检查正常、异常、看门狗、内存等退出情况
}
MXCallStackTree 附加到每个诊断信息(崩溃、卡顿、CPU 异常、磁盘写入、应用启动)。使用 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 进行符号化,或将 dSYMs 上传到您的分析服务。
可用性 : MXCallStackTree — iOS 14.0+, macOS 12.0+, visionOS 1.0+
使用带有 MetricKit 日志句柄的 mxSignpost 来捕获自定义性能区间。这些将出现在每日 MXMetricPayload 的 signpostMetrics 下。
let metricLog = MXMetricManager.makeLogHandle(category: "Networking")
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
}
}
}
系统限制每个日志的自定义标记指标数量,以减少设备上的开销。请将自定义指标保留给关键代码路径使用。
两种数据类型都符合 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)
// 执行扩展启动工作...
await database.load()
MXMetricManager.shared.finishExtendedLaunchMeasurement(forTaskID: taskID)
扩展启动时间会出现在 MXAppLaunchMetric 的 histogrammedExtendedLaunch 下。
Xcode Organizer 显示相同的 MetricKit 数据,这些数据在所有选择共享诊断信息的用户中聚合。使用 Organizer 进行趋势分析:
设备上的 MetricKit 收集是对 Organizer 的补充,它允许您将原始数据路由到自己的后端,以便按用户群组进行自定义仪表板、告警和过滤。
系统可能在启动后不久交付待处理的数据。过晚订阅(例如在视图控制器中)有完全错过它们的风险。
// 错误 — 在视图控制器中订阅
override func viewDidLoad() {
super.viewDidLoad()
MXMetricManager.shared.add(self)
}
// 正确 — 在 application(_:didFinishLaunchingWithOptions:) 中订阅
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions opts: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
MXMetricManager.shared.add(metricsSubscriber)
return true
}
仅处理 MXMetricPayload 意味着您会错过崩溃、卡顿和磁盘写入诊断——这是 MetricKit 提供的最具可操作性的数据。
// 错误 — 仅实现指标回调
func didReceive(_ payloads: [MXMetricPayload]) { /* ... */ }
// 正确 — 实现两个回调
func didReceive(_ payloads: [MXMetricPayload]) { /* ... */ }
func didReceive(_ payloads: [MXDiagnosticPayload]) { /* ... */ }
系统只交付每个数据一次。如果您的订阅者在处理过程中崩溃,数据将永久丢失。
// 错误 — 内联处理,崩溃导致数据丢失
func didReceive(_ payloads: [MXDiagnosticPayload]) {
for p in payloads {
riskyProcessing(p) // 如果此处崩溃,数据就没了
}
}
// 正确 — 先持久化原始 JSON,然后处理
func didReceive(_ payloads: [MXDiagnosticPayload]) {
for p in payloads {
let json = p.jsonRepresentation()
try? json.write(to: localCacheURL()) // 安全地存储在磁盘上
Task.detached { self.processAsync(json) }
}
}
回调在任意线程上运行。用繁重的处理或同步网络调用阻塞它会延迟后续数据的交付。
// 错误 — 在回调中同步上传
func didReceive(_ payloads: [MXMetricPayload]) {
for p in payloads {
let data = p.jsonRepresentation()
URLSession.shared.uploadTask(with: request, from: data).resume() // 同步等待
}
}
// 正确 — 持久化并异步分发
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 或模拟数据来加快迭代速度。
MXMetricManager.shared.add(subscriber) 在 application(_:didFinishLaunchingWithOptions:) 或 App.init 中调用MXMetricManagerSubscriber 协议并继承 NSObjectdidReceive(_: [MXMetricPayload]) 和 didReceive(_: [MXDiagnosticPayload])jsonRepresentation() 已持久化到磁盘MXCallStackTree JSON 已与 dSYMs 一起上传以进行符号化pastPayloads 和 pastDiagnosticPayloads 以获取错过的交付extendLaunchMeasurement 和 finishExtendedLaunchMeasurement每周安装量
330
代码仓库
GitHub 星标数
269
首次出现
2026年3月8日
安全审计
安装于
codex327
opencode324
github-copilot324
amp324
cline324
kimi-cli324
Collect aggregated performance metrics and crash diagnostics from production devices using MetricKit. The framework delivers daily metric payloads (CPU, memory, launch time, hang rate, animation hitches, network usage) and immediate diagnostic payloads (crashes, hangs, disk-write exceptions) with full call-stack trees for triage.
Register a subscriber as early as possible — ideally in application(_:didFinishLaunchingWithOptions:) or App.init. MetricKit starts accumulating reports after the first access to MXMetricManager.shared.
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)
}
}
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
MetricsSubscriber.shared.subscribe()
return true
}
@main
struct MyApp: App {
init() {
MetricsSubscriber.shared.subscribe()
}
var body: some Scene {
WindowGroup { ContentView() }
}
}
MXMetricPayload arrives approximately once per 24 hours containing aggregated metrics. The array may contain multiple payloads if prior deliveries were missed.
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)
}
}
Availability : MXMetricPayload — iOS 13.0+, macOS 10.15+, visionOS 1.0+
MXDiagnosticPayload delivers crash, hang, CPU exception, disk-write, and app-launch diagnostics. On iOS 15+ and macOS 12+, diagnostics arrive immediately rather than bundled with the daily report.
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)
}
}
}
}
Availability : MXDiagnosticPayload — iOS 14.0+, macOS 12.0+, visionOS 1.0+
if let launch = payload.applicationLaunchMetrics {
let firstDraw = launch.histogrammedTimeToFirstDraw
let optimized = launch.histogrammedOptimizedTimeToFirstDraw
let resume = launch.histogrammedApplicationResumeTime
let extended = launch.histogrammedExtendedLaunch
}
if let runTime = payload.applicationTimeMetrics {
let fg = runTime.cumulativeForegroundTime // Measurement<UnitDuration>
let bg = runTime.cumulativeBackgroundTime
let bgAudio = runTime.cumulativeBackgroundAudioTime
let bgLocation = runTime.cumulativeBackgroundLocationTime
}
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.
}
MXCallStackTree is attached to each diagnostic (crash, hang, CPU exception, disk write, app launch). Use jsonRepresentation() to extract and symbolicate.
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())
}
The JSON structure contains an array of call stack frames with binary name, offset, and address. Symbolicate using atos or upload dSYMs to your analytics service.
Availability : MXCallStackTree — iOS 14.0+, macOS 12.0+, visionOS 1.0+
Use mxSignpost with a MetricKit log handle to capture custom performance intervals. These appear in the daily MXMetricPayload under signpostMetrics.
let metricLog = MXMetricManager.makeLogHandle(category: "Networking")
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
}
}
}
The system limits the number of custom signpost metrics per log to reduce on-device overhead. Reserve custom metrics for critical code paths.
Both payload types conform to NSSecureCoding and provide jsonRepresentation() for easy serialization.
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)
}
}
If the subscriber was not registered when payloads arrived, retrieve them using pastPayloads and pastDiagnosticPayloads. These return reports generated since the last allocation of the shared manager.
let pastMetrics = MXMetricManager.shared.pastPayloads
let pastDiags = MXMetricManager.shared.pastDiagnosticPayloads
Track post-first-draw setup work (loading databases, restoring state) as part of the launch metric using extended launch measurement.
let taskID = MXLaunchTaskID("com.example.app.loadDatabase")
MXMetricManager.shared.extendLaunchMeasurement(forTaskID: taskID)
// Perform extended launch work...
await database.load()
MXMetricManager.shared.finishExtendedLaunchMeasurement(forTaskID: taskID)
Extended launch times appear under histogrammedExtendedLaunch in MXAppLaunchMetric.
Xcode Organizer shows the same MetricKit data aggregated across all users who have opted in to share diagnostics. Use Organizer for trend analysis:
MetricKit on-device collection complements Organizer by letting you route raw data to your own backend for custom dashboards, alerting, and filtering by user cohort.
The system may deliver pending payloads shortly after launch. Subscribing late (e.g., in a view controller) risks missing them entirely.
// 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
}
Only handling MXMetricPayload means you miss crash, hang, and disk-write diagnostics — the most actionable data MetricKit provides.
// WRONG — only implementing metric callback
func didReceive(_ payloads: [MXMetricPayload]) { /* ... */ }
// CORRECT — implement both callbacks
func didReceive(_ payloads: [MXMetricPayload]) { /* ... */ }
func didReceive(_ payloads: [MXDiagnosticPayload]) { /* ... */ }
The system delivers each payload once. If your subscriber crashes during processing, the data is lost permanently.
// 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) }
}
}
The callback runs on an arbitrary thread. Blocking it with heavy processing or synchronous network calls delays delivery of subsequent payloads.
// 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 aggregates data over 24-hour windows. Payloads do not arrive immediately after instrumenting. Use Xcode Organizer or simulated payloads for faster iteration during development.
MXMetricManager.shared.add(subscriber) called in application(_:didFinishLaunchingWithOptions:) or App.initMXMetricManagerSubscriber and inherits NSObjectdidReceive(_: [MXMetricPayload]) and didReceive(_: [MXDiagnosticPayload]) implementedjsonRepresentation() persisted to disk before processingMXCallStackTree JSON uploaded with dSYMs for symbolicationWeekly Installs
330
Repository
GitHub Stars
269
First Seen
Mar 8, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex327
opencode324
github-copilot324
amp324
cline324
kimi-cli324
ESLint迁移到Oxlint完整指南:JavaScript/TypeScript项目性能优化工具
1,300 周安装
Notion规范转实现计划工具:AI驱动项目管理,自动生成任务与跟踪进度
495 周安装
React Native 测试模式与工具:TDD、工厂模式、模拟模块实战指南
472 周安装
Tailwind v4 + shadcn/ui 生产级技术栈配置指南:5分钟快速搭建,避免常见错误
511 周安装
批判性思维与逻辑推理指南 - 提升AI智能体分析能力,避免信号稀释与语境坍塌
533 周安装
Three.js 3D Web开发教程 - WebGL/WebGPU图形编程、动画与性能优化指南
484 周安装
用户故事拆分指南:8种模式分解大型故事,提升敏捷开发效率
518 周安装
pastPayloads and pastDiagnosticPayloads checked on launch for missed deliveriesextendLaunchMeasurement and finishExtendedLaunchMeasurement