mcpField
A
axiom-metrickit-ref

axiom-metrickit-ref

github.com/charleswiltgen开发1.2k163 次安装今天更新
代码仓库

关于 axiom-metrickit-ref

面向 iOS 开发者的 MetricKit API 参考,涵盖性能指标、诊断载荷及调用栈符号化,帮助理解和处理用户设备汇总数据。

主要功能

  1. 01MetricKit 订阅设置
  2. 02性能指标载荷解析
  3. 03崩溃与卡顿诊断解析
  4. 04调用栈符号化指引
  5. 05后台退出原因分析
  6. 06载荷导出与上报模式

适用场景

  1. 01接入 MetricKit 数据订阅
  2. 02分析 CPU、内存与启动表现
  3. 03排查崩溃、卡顿及 Jetsam
  4. 04将诊断数据整合至崩溃报告工具

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/charleswiltgen/axiom --skill axiom-metrickit-ref

安装后在 Claude Code 中输入/axiom-metrickit-ref确认已加载。

详细介绍

MetricKit API 参考

使用 MetricKit 收集现场性能指标和诊断信息的完整 API 参考。

概述

MetricKit 为选择共享分析数据的用户提供聚合的设备端性能和诊断数据。数据每天传送一次(开发期间可按需传送)。

何时使用此参考

在以下情况下使用此参考:

  • 在应用中设置 MetricKit 订阅者
  • 解析 MXMetricPayload 或 MXDiagnosticPayload
  • 对 MXCallStackTree 崩溃数据进行符号化
  • 了解后台退出原因(jetsam、watchdog)
  • 将 MetricKit 与现有崩溃报告工具集成

有关卡顿诊断工作流,请参阅 axiom-hang-diagnostics。 有关使用 Instruments 进行常规性能分析,请参阅 axiom-performance-profiling。 有关包含 jetsam 的内存调试,请参阅 axiom-memory-debugging。

常见注意事项

  1. 延迟 24 小时 — MetricKit 数据每天送达一次;它不是实时调试工具
  2. 调用栈需要符号化 — MXCallStackTree 帧未经符号化;请保留 dSYM
  3. 仅限选择加入的用户 — 只有启用了“与 App 开发者共享”的用户才会贡献数据
  4. 数据经过聚合,并非针对单个用户 — 你会获得计数和平均值,而非每位用户的跟踪记录
  5. 模拟器无法使用 — MetricKit 仅在实体设备上收集数据

iOS 版本支持:

功能iOS 版本
基本指标(电池、CPU、内存)iOS 13+
诊断负载iOS 14+
卡顿诊断iOS 14+
启动诊断iOS 16+
开发期间即时传送iOS 15+

第 1 部分:设置

基本集成
import MetricKit

class AppMetricsSubscriber: NSObject, MXMetricManagerSubscriber {

    override init() {
        super.init()
        MXMetricManager.shared.add(self)
    }

    deinit {
        MXMetricManager.shared.remove(self)
    }

    // MARK: - MXMetricManagerSubscriber

    func didReceive(_ payloads: [MXMetricPayload]) {
        for payload in payloads {
            processMetrics(payload)
        }
    }

    func didReceive(_ payloads: [MXDiagnosticPayload]) {
        for payload in payloads {
            processDiagnostics(payload)
        }
    }
}
注册时机

在应用生命周期早期注册订阅者:

@main
struct MyApp: App {
    @StateObject private var metricsSubscriber = AppMetricsSubscriber()

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

也可以在 AppDelegate 中注册:

func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    metricsSubscriber = AppMetricsSubscriber()
    return true
}
开发测试

在 iOS 15+ 中,通过 Debug 菜单触发即时传送:

Xcode > Debug > Simulate MetricKit Payloads

也可以通过编程方式触发(仅限调试版本):

#if DEBUG
// Payloads delivered immediately in development
// No special code needed - just run and wait
#endif

第 2 部分:MXMetricPayload

MXMetricPayload 包含过去 24 小时的聚合性能指标。

负载结构
func processMetrics(_ payload: MXMetricPayload) {
    // Time range for this payload
    let start = payload.timeStampBegin
    let end = payload.timeStampEnd

    // App version that generated this data
    let version = payload.metaData?.applicationBuildVersion

    // Access specific metric categories
    if let cpuMetrics = payload.cpuMetrics {
        processCPU(cpuMetrics)
    }

    if let memoryMetrics = payload.memoryMetrics {
        processMemory(memoryMetrics)
    }

    if let launchMetrics = payload.applicationLaunchMetrics {
        processLaunches(launchMetrics)
    }

    // ... other categories
}
CPU 指标(MXCPUMetric)
func processCPU(_ metrics: MXCPUMetric) {
    // Cumulative CPU time
    let cpuTime = metrics.cumulativeCPUTime  // Measurement<UnitDuration>

    // iOS 14+: CPU instruction count
    if #available(iOS 14.0, *) {
        let instructions = metrics.cumulativeCPUInstructions  // Measurement<Unit>
    }
}
内存指标(MXMemoryMetric)
func processMemory(_ metrics: MXMemoryMetric) {
    // Peak memory usage
    let peakMemory = metrics.peakMemoryUsage  // Measurement<UnitInformationStorage>

    // Average suspended memory
    let avgSuspended = metrics.averageSuspendedMemory  // MXAverage<UnitInformationStorage>
}
启动指标(MXAppLaunchMetric)
func processLaunches(_ metrics: MXAppLaunchMetric) {
    // First draw (cold launch) histogram
    let firstDrawHistogram = metrics.histogrammedTimeToFirstDraw

    // Resume time histogram
    let resumeHistogram = metrics.histogrammedApplicationResumeTime

    // Optimized time to first draw (iOS 15.2+)
    if #available(iOS 15.2, *) {
        let optimizedLaunch = metrics.histogrammedOptimizedTimeToFirstDraw
    }

    // Parse histogram buckets
    for bucket in firstDrawHistogram.bucketEnumerator {
        if let bucket = bucket as? MXHistogramBucket<UnitDuration> {
            let start = bucket.bucketStart  // e.g., 0ms
            let end = bucket.bucketEnd      // e.g., 100ms
            let count = bucket.bucketCount  // Number of launches in this range
        }
    }
}
应用退出指标(MXAppExitMetric)— iOS 14+
@available(iOS 14.0, *)
func processExits(_ metrics: MXAppExitMetric) {
    let fg = metrics.foregroundExitData
    let bg = metrics.backgroundExitData

    // Foreground (onscreen) exits
    let fgNormal = fg.cumulativeNormalAppExitCount
    let fgWatchdog = fg.cumulativeAppWatchdogExitCount
    let fgMemoryLimit = fg.cumulativeMemoryResourceLimitExitCount
    let fgMemoryPressure = fg.cumulativeMemoryPressureExitCount
    let fgBadAccess = fg.cumulativeBadAccessExitCount
    let fgIllegalInstruction = fg.cumulativeIllegalInstructionExitCount
    let fgAbnormal = fg.cumulativeAbnormalExitCount

    // Background exits
    let bgSuspended = bg.cumulativeSuspendedWithLockedFileExitCount
    let bgTaskTimeout = bg.cumulativeBackgroundTaskAssertionTimeoutExitCount
    let bgCPULimit = bg.cumulativeCPUResourceLimitExitCount
}
滚动卡顿指标(MXAnimationMetric)— iOS 14+
@available(iOS 14.0, *)
func processHitches(_ metrics: MXAnimationMetric) {
    // Scroll hitch rate (hitches per scroll)
    let scrollHitchRate = metrics.scrollHitchTimeRatio  // Double (0.0 - 1.0)
}
磁盘 I/O 指标(MXDiskIOMetric)
func processDiskIO(_ metrics: MXDiskIOMetric) {
    let logicalWrites = metrics.cumulativeLogicalWrites  // Measurement<UnitInformationStorage>
}
网络传输指标(MXNetworkTransferMetric)
func processNetwork(_ metrics: MXNetworkTransferMetric) {
    let cellUpload = metrics.cumulativeCellularUpload
    let cellDownload = metrics.cumulativeCellularDownload
    let wifiUpload = metrics.cumulativeWifiUpload
    let wifiDownload = metrics.cumulativeWifiDownload
}
Signpost 指标(MXSignpostMetric)

使用 signpost 跟踪自定义操作:

// In your code: emit signposts
import os.signpost

let log = MXMetricManager.makeLogHandle(category: "ImageProcessing")

func processImage(_ image: UIImage) {
    mxSignpost(.begin, log: log, name: "ProcessImage")
    // ... do work ...
    mxSignpost(.end, log: log, name: "ProcessImage")
}

// In metrics subscriber: read signpost data
func processSignposts(_ metrics: MXSignpostMetric) {
    let name = metrics.signpostName
    let category = metrics.signpostCategory

    // Histogram of durations
    let histogram = metrics.signpostIntervalData.histogrammedSignpostDurations

    // Total count
    let count = metrics.totalCount
}
将负载导出为 JSON
func exportPayload(_ payload: MXMetricPayload) {
    // JSON representation for upload to analytics
    let jsonData = payload.jsonRepresentation()

    // Or as Dictionary
    if let json = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any] {
        uploadToAnalytics(json)
    }
}

第 3 部分:MXDiagnosticPayload — iOS 14+

MXDiagnosticPayload 包含崩溃、卡顿、磁盘写入异常和 CPU 异常的诊断报告。

负载结构
@available(iOS 14.0, *)
func processDiagnostics(_ payload: MXDiagnosticPayload) {
    // Crash diagnostics
    if let crashes = payload.crashDiagnostics {
        for crash in crashes {
            processCrash(crash)
        }
    }

    // Hang diagnostics
    if let hangs = payload.hangDiagnostics {
        for hang in hangs {
            processHang(hang)
        }
    }

    // Disk write exceptions
    if let diskWrites = payload.diskWriteExceptionDiagnostics {
        for diskWrite in diskWrites {
            processDiskWriteException(diskWrite)
        }
    }

    // CPU exceptions
    if let cpuExceptions = payload.cpuExceptionDiagnostics {
        for cpuException in cpuExceptions {
            processCPUException(cpuException)
        }
    }
}
MXCrashDiagnostic
@available(iOS 14.0, *)
func processCrash(_ diagnostic: MXCrashDiagnostic) {
    // Call stack tree (needs symbolication)
    let callStackTree = diagnostic.callStackTree

    // Crash metadata
    let signal = diagnostic.signal              // e.g., SIGSEGV
    let exceptionType = diagnostic.exceptionType  // e.g., EXC_BAD_ACCESS
    let exceptionCode = diagnostic.exceptionCode
    let terminationReason = diagnostic.terminationReason

    // Virtual memory info
    let virtualMemoryRegionInfo = diagnostic.virtualMemoryRegionInfo

    // Unique identifier for grouping similar crashes
    // (not available - use call stack signature)
}
MXHangDiagnostic
@available(iOS 14.0, *)
func processHang(_ diagnostic: MXHangDiagnostic) {
    // How long the hang lasted
    let duration = diagnostic.hangDuration  // Measurement<UnitDuration>

    // Call stack when hang occurred
    let callStackTree = diagnostic.callStackTree
}
MXDiskWriteExceptionDiagnostic
@available(iOS 14.0, *)
func processDiskWriteException(_ diagnostic: MXDiskWriteExceptionDiagnostic) {
    // Total bytes written that triggered exception
    let totalWrites = diagnostic.totalWritesCaused  // Measurement<UnitInformationStorage>

    // Call stack of writes
    let callStackTree = diagnostic.callStackTree
}
MXCPUExceptionDiagnostic
@available(iOS 14.0, *)
func processCPUException(_ diagnostic: MXCPUExceptionDiagnostic) {
    // Total CPU time that triggered exception
    let totalCPUTime = diagnostic.totalCPUTime  // Measurement<UnitDuration>

    // Total sampled time
    let totalSampledTime = diagnostic.totalSampledTime

    // Call stack of CPU-intensive code
    let callStackTree = diagnostic.callStackTree
}

第 4 部分:MXCallStackTree

MXCallStackTree 包含诊断数据中的栈帧。栈帧未经符号化——你必须使用 dSYM 进行符号化。

结构
@available(iOS 14.0, *)
func parseCallStackTree(_ tree: MXCallStackTree) {
    // JSON representation
    let jsonData = tree.jsonRepresentation()

    // Parse the JSON
    guard let json = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any],
          let callStacks = json["callStacks"] as? [[String: Any]] else {
        return
    }

    for callStack in callStacks {
        guard let threadAttributed = callStack["threadAttributed"] as? Bool,
              let frames = callStack["callStackRootFrames"] as? [[String: Any]] else {
            continue
        }

        // threadAttributed = true means this thread caused the issue
        if threadAttributed {
            parseFrames(frames)
        }
    }
}

func parseFrames(_ frames: [[String: Any]]) {
    for frame in frames {
        // Binary image UUID (match to dSYM)
        let binaryUUID = frame["binaryUUID"] as? String

        // Address offset within binary
        let offsetIntoBinaryTextSegment = frame["offsetIntoBinaryTextSegment"] as? Int

        // Binary name (e.g., "MyApp", "UIKitCore")
        let binaryName = frame["binaryName"] as? String

        // Address (for symbolication)
        let address = frame["address"] as? Int

        // Sample count (how many times this frame appeared)
        let sampleCount = frame["sampleCount"] as? Int

        // Sub-frames (tree structure)
        let subFrames = frame["subFrames"] as? [[String: Any]]
    }
}
JSON 结构示例
{
  "callStacks": [
    {
      "threadAttributed": true,
      "callStackRootFrames": [
        {
          "binaryUUID": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890",
          "offsetIntoBinaryTextSegment": 123456,
          "binaryName": "MyApp",
          "address": 4384712345,
          "sampleCount": 10,
          "subFrames": [
            {
              "binaryUUID": "F1E2D3C4-B5A6-7890-1234-567890ABCDEF",
              "offsetIntoBinaryTextSegment": 78901,
              "binaryName": "UIKitCore",
              "address": 7234567890,
              "sampleCount": 10
            }
          ]
        }
      ]
    }
  ]
}
符号化

MetricKit 调用栈未经符号化。要进行符号化:

  1. 保留每个 App Store 构建版本的 dSYM 文件
  2. 匹配 UUID:将 binaryUUID 与 dSYM 对应
  3. 使用 atos 进行符号化:
# Find dSYM for binary UUID
mdfind "com_apple_xcode_dsym_uuids == A1B2C3D4-E5F6-7890-ABCD-EF1234567890"

# Symbolicate address
atos -arch arm64 -o MyApp.app.dSYM/Contents/Resources/DWARF/MyApp -l 0x100000000 0x105234567

也可以使用能够处理符号化的崩溃报告服务(Crashlytics、Sentry 等)。

第 5 部分:MXBackgroundExitData

跟踪应用在后台终止的原因:

@available(iOS 14.0, *)
func analyzeBackgroundExits(_ data: MXBackgroundExitData) {
    // Normal exits (user closed, system reclaimed)
    let normal = data.cumulativeNormalAppExitCount

    // Memory issues
    let memoryLimit = data.cumulativeMemoryResourceLimitExitCount  // Exceeded memory limit
    let memoryPressure = data.cumulativeMemoryPressureExitCount    // Jetsam

    // Crashes
    let badAccess = data.cumulativeBadAccessExitCount        // SIGSEGV
    let illegalInstruction = data.cumulativeIllegalInstructionExitCount  // SIGILL
    let abnormal = data.cumulativeAbnormalExitCount          // Other crashes

    // System terminations
    let watchdog = data.cumulativeAppWatchdogExitCount       // Timeout during transition
    let taskTimeout = data.cumulativeBackgroundTaskAssertionTimeoutExitCount  // Background task timeout
    let cpuLimit = data.cumulativeCPUResourceLimitExitCount  // Exceeded CPU quota
    let lockedFile = data.cumulativeSuspendedWithLockedFileExitCount  // File lock held
}
退出类型解读
退出类型含义操作
normalAppExitCount正常退出无(符合预期)
memoryResourceLimitExitCount内存使用过多减少内存占用
memoryPressureExitCountJetsam(系统回收)将后台内存减少到 <50MB
badAccessExitCountSIGSEGV 崩溃检查空指针和无效内存
illegalInstructionExitCountSIGILL 崩溃检查无效函数指针
abnormalExitCount其他崩溃检查崩溃诊断信息
appWatchdogExitCount转场期间无响应减少启动和后台工作
backgroundTaskAssertionTimeoutExitCount未结束后台任务正确调用 endBackgroundTask
cpuResourceLimitExitCount后台 CPU 使用过多改用 BGProcessingTask
suspendedWithLockedFileExitCount挂起时持有文件锁挂起前释放锁

第 6 部分:集成模式

上传到分析服务
class MetricsUploader {
    func upload(_ payload: MXMetricPayload) {
        let jsonData = payload.jsonRepresentation()

        var request = URLRequest(url: analyticsEndpoint)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpBody = jsonData

        URLSession.shared.dataTask(with: request) { _, response, error in
            if let error = error {
                // Queue for retry
                self.queueForRetry(jsonData)
            }
        }.resume()
    }
}
与崩溃报告工具结合
class HybridCrashReporter: MXMetricManagerSubscriber {
    let crashlytics: Crashlytics // or Sentry, etc.

    func didReceive(_ payloads: [MXDiagnosticPayload]) {
        for payload in payloads {
            // MetricKit captures crashes that traditional reporters might miss
            // (e.g., watchdog kills, memory pressure exits)

            if let crashes = payload.crashDiagnostics {
                for crash in crashes {
                    crashlytics.recordException(
                        name: crash.exceptionType?.description ?? "Unknown",
                        reason: crash.terminationReason ?? "MetricKit crash",
                        callStack: parseCallStack(crash.callStackTree)
                    )
                }
            }
        }
    }
}
在出现回归时发出警报
class MetricsMonitor: MXMetricManagerSubscriber {
    let thresholds = MetricThresholds(
        launchTime: 2.0,  // seconds
        hangRate: 0.01,   // 1% of sessions
        memoryPeak: 200   // MB
    )

    func didReceive(_ payloads: [MXMetricPayload]) {
        for payload in payloads {
            checkThresholds(payload)
        }
    }

    private func checkThresholds(_ payload: MXMetricPayload) {
        // Check launch time
        if let launches = payload.applicationLaunchMetrics {
            let p50 = calculateP50(launches.histogrammedTimeToFirstDraw)
            if p50 > thresholds.launchTime {
                sendAlert("Launch time regression: \(p50)s > \(thresholds.launchTime)s")
            }
        }

        // Check memory
        if let memory = payload.memoryMetrics {
            let peakMB = memory.peakMemoryUsage.converted(to: .megabytes).value
            if peakMB > Double(thresholds.memoryPeak) {
                sendAlert("Memory peak regression: \(peakMB)MB > \(thresholds.memoryPeak)MB")
            }
        }
    }
}

第 7 部分:最佳实践

应该做的事
  • 尽早注册订阅者 — 在 application(_:didFinishLaunchingWithOptions:) 或 App 初始化时注册
  • 保留 dSYM 文件 — 符号化调用栈必需
  • 将负载上传到服务端 — 本地处理会在卸载后丢失数据
  • 设置警报 — 在用户报告之前发现回归
  • 使用模拟负载进行测试 — iOS 15 及更高版本的 Xcode Debug 菜单
不应该做的事
  • 不要只依赖 MetricKit — 有 24 小时延迟,且需要用户选择加入
  • 不要忽略后台退出 — Jetsam 和任务超时会影响用户体验
  • 不要跳过符号化 — 原始地址无法使用
  • 不要在主线程上处理 — 处理负载可能开销很大
隐私注意事项
  • MetricKit 数据经过聚合和匿名化
  • 数据仅来自选择共享分析数据的用户
  • 不包含个人身份信息
  • 可以安全地上传到你的服务端

第 8 部分:MetricKit 与 Xcode Organizer 对比

功能MetricKitXcode Organizer
数据来源运行你应用的设备App Store Connect 聚合数据
数据交付每日发送给订阅者在 Xcode 中按需查看
自定义程度可完全访问原始数据预定义视图
符号化必须自行符号化已预先符号化
历史数据仅在订阅者处于活动状态时提供最近 16 个版本
需要代码是否

两者都用:使用 Organizer 快速概览,使用 MetricKit 进行自定义分析和设置警报。

资源

WWDC:2019-417、2020-10081、2021-10087

文档:/metrickit、/metrickit/mxmetricmanager、/metrickit/mxdiagnosticpayload

技能:axiom-hang-diagnostics、axiom-performance-profiling、axiom-testflight-triage

评论

支持 Markdown