mcpField
M
metrickit-diagnostics

metrickit-diagnostics

github.com/dpearson2699开发1.2k409 次安装13 周前更新
代码仓库

关于 metrickit-diagnostics

面向 Apple 平台应用开发者的 MetricKit 诊断技能,说明如何收集性能指标与崩溃等诊断数据,并将数据持久化、分析或上传。

主要功能

  1. 01订阅并接收指标与诊断负载
  2. 02解析启动、CPU、内存和网络指标
  3. 03提取调用栈并用于符号化
  4. 04采集自定义 Signpost 指标
  5. 05持久化及上传 JSON 负载
  6. 06测量扩展启动任务

适用场景

  1. 01排查线上应用的崩溃与卡顿
  2. 02分析版本间的性能变化
  3. 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() 持久化到磁盘
  • 将繁重处理异步分派到回调线程之外
  • 上传 MXCallStackTree JSON 和 dSYM,以便进行符号化
  • 将自定义 signpost 指标限制在关键代码路径中
  • 启动时检查 pastPayloads 和 pastDiagnosticPayloads,确认是否有遗漏的交付
  • 扩展启动任务同时调用 extendLaunchMeasurement 和 finishExtendedLaunchMeasurement
  • 分析后端能够接受并存储 MetricKit JSON 格式
  • 查看 Xcode Organizer 中的回归趋势,并与设备端数据对照

参考资料

评论

支持 Markdown