关于 axiom-metrickit-ref
面向 iOS 开发者的 MetricKit API 参考,涵盖性能指标、诊断载荷及调用栈符号化,帮助理解和处理用户设备汇总数据。
主要功能
- 01MetricKit 订阅设置
- 02性能指标载荷解析
- 03崩溃与卡顿诊断解析
- 04调用栈符号化指引
- 05后台退出原因分析
- 06载荷导出与上报模式
适用场景
- 01接入 MetricKit 数据订阅
- 02分析 CPU、内存与启动表现
- 03排查崩溃、卡顿及 Jetsam
- 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。
常见注意事项
- 延迟 24 小时 — MetricKit 数据每天送达一次;它不是实时调试工具
- 调用栈需要符号化 — MXCallStackTree 帧未经符号化;请保留 dSYM
- 仅限选择加入的用户 — 只有启用了“与 App 开发者共享”的用户才会贡献数据
- 数据经过聚合,并非针对单个用户 — 你会获得计数和平均值,而非每位用户的跟踪记录
- 模拟器无法使用 — 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 调用栈未经符号化。要进行符号化:
- 保留每个 App Store 构建版本的 dSYM 文件
- 匹配 UUID:将
binaryUUID与 dSYM 对应 - 使用 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 | 内存使用过多 | 减少内存占用 |
memoryPressureExitCount | Jetsam(系统回收) | 将后台内存减少到 <50MB |
badAccessExitCount | SIGSEGV 崩溃 | 检查空指针和无效内存 |
illegalInstructionExitCount | SIGILL 崩溃 | 检查无效函数指针 |
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 对比
| 功能 | MetricKit | Xcode 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