axiom-metrickit-ref
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMetricKit API Reference
MetricKit API 参考文档
Complete API reference for collecting field performance metrics and diagnostics using MetricKit.
使用MetricKit收集现场性能指标与诊断数据的完整API参考。
Overview
概述
MetricKit provides aggregated, on-device performance and diagnostic data from users who opt into sharing analytics. Data is delivered daily (or on-demand in development).
MetricKit 提供来自选择共享分析数据的用户的聚合式设备端性能与诊断数据。数据每日推送(开发环境下可按需获取)。
When to Use This Reference
何时使用本参考文档
Use this reference when:
- Setting up MetricKit subscriber in your app
- Parsing MXMetricPayload or MXDiagnosticPayload
- Symbolicating MXCallStackTree crash data
- Understanding background exit reasons (jetsam, watchdog)
- Integrating MetricKit with existing crash reporters
For hang diagnosis workflows, see .
For general profiling with Instruments, see .
For memory debugging including jetsam, see .
axiom-hang-diagnosticsaxiom-performance-profilingaxiom-memory-debugging在以下场景中使用本参考:
- 在应用中设置MetricKit订阅者
- 解析MXMetricPayload或MXDiagnosticPayload
- 对MXCallStackTree崩溃数据进行符号化
- 理解后台退出原因(jetsam、看门狗)
- 将MetricKit与现有崩溃报告工具集成
有关挂起诊断工作流,请查看。
有关使用Instruments进行常规性能分析,请查看。
有关包括jetsam在内的内存调试,请查看。
axiom-hang-diagnosticsaxiom-performance-profilingaxiom-memory-debuggingCommon Gotchas
常见注意事项
- 24-hour delay — MetricKit data arrives once daily; it's not real-time debugging
- Call stacks require symbolication — MXCallStackTree frames are unsymbolicated; keep dSYMs
- Opt-in only — Only users who enable "Share with App Developers" contribute data
- Aggregated, not individual — You get counts and averages, not per-user traces
- Simulator doesn't work — MetricKit only collects on physical devices
iOS Version Support:
| Feature | iOS Version |
|---|---|
| Basic metrics (battery, CPU, memory) | iOS 13+ |
| Diagnostic payloads | iOS 14+ |
| Hang diagnostics | iOS 14+ |
| Launch diagnostics | iOS 16+ |
| Immediate delivery in dev | iOS 15+ |
- 24小时延迟 — MetricKit数据每日推送一次;不支持实时调试
- 调用栈需要符号化 — MXCallStackTree的栈帧未经过符号化;请保留dSYM文件
- 仅选择加入的用户 — 只有启用了“与应用开发者共享”的用户才会贡献数据
- 聚合数据而非个体数据 — 你将获得统计数量与平均值,而非单个用户的追踪数据
- 模拟器不支持 — MetricKit仅在物理设备上收集数据
iOS版本支持:
| 功能 | iOS版本 |
|---|---|
| 基础指标(电池、CPU、内存) | iOS 13+ |
| 诊断负载 | iOS 14+ |
| 挂起诊断 | iOS 14+ |
| 启动诊断 | iOS 16+ |
| 开发环境下即时推送 | iOS 15+ |
Part 1: Setup
第一部分:设置
Basic Integration
基础集成
swift
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)
}
}
}swift
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)
}
}
}Registration Timing
注册时机
Register subscriber early in app lifecycle:
swift
@main
struct MyApp: App {
@StateObject private var metricsSubscriber = AppMetricsSubscriber()
var body: some Scene {
WindowGroup {
ContentView()
}
}
}Or in AppDelegate:
swift
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
metricsSubscriber = AppMetricsSubscriber()
return true
}在应用生命周期早期注册订阅者:
swift
@main
struct MyApp: App {
@StateObject private var metricsSubscriber = AppMetricsSubscriber()
var body: some Scene {
WindowGroup {
ContentView()
}
}
}或在AppDelegate中:
swift
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
metricsSubscriber = AppMetricsSubscriber()
return true
}Development Testing
开发测试
In iOS 15+, trigger immediate delivery via Debug menu:
Xcode > Debug > Simulate MetricKit Payloads
Or programmatically (debug builds only):
swift
#if DEBUG
// Payloads delivered immediately in development
// No special code needed - just run and wait
#endif在iOS 15及以上版本中,可通过Debug菜单触发即时推送:
Xcode > Debug > Simulate MetricKit Payloads
或通过代码实现(仅调试构建):
swift
#if DEBUG
// 开发环境下负载会即时推送
// 无需特殊代码 - 只需运行等待即可
#endifPart 2: MXMetricPayload
第二部分:MXMetricPayload
MXMetricPayloadMXMetricPayloadPayload Structure
负载结构
swift
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
}swift
func processMetrics(_ payload: MXMetricPayload) {
// 该负载的时间范围
let start = payload.timeStampBegin
let end = payload.timeStampEnd
// 生成此数据的应用版本
let version = payload.metaData?.applicationBuildVersion
// 访问特定指标类别
if let cpuMetrics = payload.cpuMetrics {
processCPU(cpuMetrics)
}
if let memoryMetrics = payload.memoryMetrics {
processMemory(memoryMetrics)
}
if let launchMetrics = payload.applicationLaunchMetrics {
processLaunches(launchMetrics)
}
// ... 其他类别
}CPU Metrics (MXCPUMetric)
CPU指标(MXCPUMetric)
swift
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>
}
}swift
func processCPU(_ metrics: MXCPUMetric) {
// 累计CPU时间
let cpuTime = metrics.cumulativeCPUTime // Measurement<UnitDuration>
// iOS 14及以上:CPU指令计数
if #available(iOS 14.0, *) {
let instructions = metrics.cumulativeCPUInstructions // Measurement<Unit>
}
}Memory Metrics (MXMemoryMetric)
内存指标(MXMemoryMetric)
swift
func processMemory(_ metrics: MXMemoryMetric) {
// Peak memory usage
let peakMemory = metrics.peakMemoryUsage // Measurement<UnitInformationStorage>
// Average suspended memory
let avgSuspended = metrics.averageSuspendedMemory // MXAverage<UnitInformationStorage>
}swift
func processMemory(_ metrics: MXMemoryMetric) {
// 峰值内存使用量
let peakMemory = metrics.peakMemoryUsage // Measurement<UnitInformationStorage>
// 平均挂起内存
let avgSuspended = metrics.averageSuspendedMemory // MXAverage<UnitInformationStorage>
}Launch Metrics (MXAppLaunchMetric)
启动指标(MXAppLaunchMetric)
swift
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
}
}
}swift
func processLaunches(_ metrics: MXAppLaunchMetric) {
// 首次绘制(冷启动)直方图
let firstDrawHistogram = metrics.histogrammedTimeToFirstDraw
// 恢复时间直方图
let resumeHistogram = metrics.histogrammedApplicationResumeTime
// 优化后的首次绘制时间(iOS 15.2+)
if #available(iOS 15.2, *) {
let optimizedLaunch = metrics.histogrammedOptimizedTimeToFirstDraw
}
// 解析直方图桶
for bucket in firstDrawHistogram.bucketEnumerator {
if let bucket = bucket as? MXHistogramBucket<UnitDuration> {
let start = bucket.bucketStart // 例如:0ms
let end = bucket.bucketEnd // 例如:100ms
let count = bucket.bucketCount // 此范围内的启动次数
}
}
}Application Exit Metrics (MXAppExitMetric) — iOS 14+
应用退出指标(MXAppExitMetric)—— iOS 14+
swift
@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
}swift
@available(iOS 14.0, *)
func processExits(_ metrics: MXAppExitMetric) {
let fg = metrics.foregroundExitData
let bg = metrics.backgroundExitData
// 前台(屏幕显示中)退出
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
// 后台退出
let bgSuspended = bg.cumulativeSuspendedWithLockedFileExitCount
let bgTaskTimeout = bg.cumulativeBackgroundTaskAssertionTimeoutExitCount
let bgCPULimit = bg.cumulativeCPUResourceLimitExitCount
}Scroll Hitch Metrics (MXAnimationMetric) — iOS 14+
滚动卡顿指标(MXAnimationMetric)—— iOS 14+
swift
@available(iOS 14.0, *)
func processHitches(_ metrics: MXAnimationMetric) {
// Scroll hitch rate (hitches per scroll)
let scrollHitchRate = metrics.scrollHitchTimeRatio // Double (0.0 - 1.0)
}swift
@available(iOS 14.0, *)
func processHitches(_ metrics: MXAnimationMetric) {
// 滚动卡顿率(每次滚动的卡顿次数)
let scrollHitchRate = metrics.scrollHitchTimeRatio // Double (0.0 - 1.0)
}Disk I/O Metrics (MXDiskIOMetric)
磁盘I/O指标(MXDiskIOMetric)
swift
func processDiskIO(_ metrics: MXDiskIOMetric) {
let logicalWrites = metrics.cumulativeLogicalWrites // Measurement<UnitInformationStorage>
}swift
func processDiskIO(_ metrics: MXDiskIOMetric) {
let logicalWrites = metrics.cumulativeLogicalWrites // Measurement<UnitInformationStorage>
}Network Metrics (MXNetworkTransferMetric)
网络指标(MXNetworkTransferMetric)
swift
func processNetwork(_ metrics: MXNetworkTransferMetric) {
let cellUpload = metrics.cumulativeCellularUpload
let cellDownload = metrics.cumulativeCellularDownload
let wifiUpload = metrics.cumulativeWifiUpload
let wifiDownload = metrics.cumulativeWifiDownload
}swift
func processNetwork(_ metrics: MXNetworkTransferMetric) {
let cellUpload = metrics.cumulativeCellularUpload
let cellDownload = metrics.cumulativeCellularDownload
let wifiUpload = metrics.cumulativeWifiUpload
let wifiDownload = metrics.cumulativeWifiDownload
}Signpost Metrics (MXSignpostMetric)
标记点指标(MXSignpostMetric)
Track custom operations with signposts:
swift
// 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
}使用标记点追踪自定义操作:
swift
// 在你的代码中:发送标记点
import os.signpost
let log = MXMetricManager.makeLogHandle(category: "ImageProcessing")
func processImage(_ image: UIImage) {
mxSignpost(.begin, log: log, name: "ProcessImage")
// ... 执行工作 ...
mxSignpost(.end, log: log, name: "ProcessImage")
}
// 在指标订阅者中:读取标记点数据
func processSignposts(_ metrics: MXSignpostMetric) {
let name = metrics.signpostName
let category = metrics.signpostCategory
// 持续时间直方图
let histogram = metrics.signpostIntervalData.histogrammedSignpostDurations
// 总计数
let count = metrics.totalCount
}Exporting Payload as JSON
将负载导出为JSON
swift
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)
}
}swift
func exportPayload(_ payload: MXMetricPayload) {
// 用于上传到分析服务的JSON表示
let jsonData = payload.jsonRepresentation()
// 或转换为字典
if let json = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any] {
uploadToAnalytics(json)
}
}Part 3: MXDiagnosticPayload — iOS 14+
第三部分:MXDiagnosticPayload —— iOS 14+
MXDiagnosticPayloadMXDiagnosticPayloadPayload Structure
负载结构
swift
@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)
}
}
}swift
@available(iOS 14.0, *)
func processDiagnostics(_ payload: MXDiagnosticPayload) {
// 崩溃诊断
if let crashes = payload.crashDiagnostics {
for crash in crashes {
processCrash(crash)
}
}
// 挂起诊断
if let hangs = payload.hangDiagnostics {
for hang in hangs {
processHang(hang)
}
}
// 磁盘写入异常
if let diskWrites = payload.diskWriteExceptionDiagnostics {
for diskWrite in diskWrites {
processDiskWriteException(diskWrite)
}
}
// CPU异常
if let cpuExceptions = payload.cpuExceptionDiagnostics {
for cpuException in cpuExceptions {
processCPUException(cpuException)
}
}
}MXCrashDiagnostic
MXCrashDiagnostic
swift
@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)
}swift
@available(iOS 14.0, *)
func processCrash(_ diagnostic: MXCrashDiagnostic) {
// 调用栈树(需要符号化)
let callStackTree = diagnostic.callStackTree
// 崩溃元数据
let signal = diagnostic.signal // 例如:SIGSEGV
let exceptionType = diagnostic.exceptionType // 例如:EXC_BAD_ACCESS
let exceptionCode = diagnostic.exceptionCode
let terminationReason = diagnostic.terminationReason
// 虚拟内存信息
let virtualMemoryRegionInfo = diagnostic.virtualMemoryRegionInfo
// 用于分组相似崩溃的唯一标识符
// (不可用 - 使用调用栈签名)
}MXHangDiagnostic
MXHangDiagnostic
swift
@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
}swift
@available(iOS 14.0, *)
func processHang(_ diagnostic: MXHangDiagnostic) {
// 挂起持续时间
let duration = diagnostic.hangDuration // Measurement<UnitDuration>
// 挂起发生时的调用栈
let callStackTree = diagnostic.callStackTree
}MXDiskWriteExceptionDiagnostic
MXDiskWriteExceptionDiagnostic
swift
@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
}swift
@available(iOS 14.0, *)
func processDiskWriteException(_ diagnostic: MXDiskWriteExceptionDiagnostic) {
// 触发异常的总写入字节数
let totalWrites = diagnostic.totalWritesCaused // Measurement<UnitInformationStorage>
// 写入操作的调用栈
let callStackTree = diagnostic.callStackTree
}MXCPUExceptionDiagnostic
MXCPUExceptionDiagnostic
swift
@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
}swift
@available(iOS 14.0, *)
func processCPUException(_ diagnostic: MXCPUExceptionDiagnostic) {
// 触发异常的总CPU时间
let totalCPUTime = diagnostic.totalCPUTime // Measurement<UnitDuration>
// 总采样时间
let totalSampledTime = diagnostic.totalSampledTime
// CPU密集型代码的调用栈
let callStackTree = diagnostic.callStackTree
}Part 4: MXCallStackTree
第四部分:MXCallStackTree
MXCallStackTreeMXCallStackTreeStructure
结构
swift
@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]]
}
}swift
@available(iOS 14.0, *)
func parseCallStackTree(_ tree: MXCallStackTree) {
// JSON表示
let jsonData = tree.jsonRepresentation()
// 解析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 表示此线程导致了问题
if threadAttributed {
parseFrames(frames)
}
}
}
func parseFrames(_ frames: [[String: Any]]) {
for frame in frames {
// 二进制镜像UUID(与dSYM匹配)
let binaryUUID = frame["binaryUUID"] as? String
// 二进制文本段内的地址偏移
let offsetIntoBinaryTextSegment = frame["offsetIntoBinaryTextSegment"] as? Int
// 二进制名称(例如:"MyApp", "UIKitCore")
let binaryName = frame["binaryName"] as? String
// 地址(用于符号化)
let address = frame["address"] as? Int
// 采样计数(此栈帧出现的次数)
let sampleCount = frame["sampleCount"] as? Int
// 子栈帧(树状结构)
let subFrames = frame["subFrames"] as? [[String: Any]]
}
}JSON Structure Example
JSON结构示例
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
}
]
}
]
}
]
}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
}
]
}
]
}
]
}Symbolication
符号化
MetricKit call stacks are unsymbolicated. To symbolicate:
- Keep your dSYM files for every App Store build
- Match UUID from to your dSYM
binaryUUID - Use atos to symbolicate:
bash
undefinedMetricKit的调用栈是未符号化的。要进行符号化:
- 保留每个App Store构建的dSYM文件
- 将与你的dSYM文件匹配
binaryUUID - 使用atos工具进行符号化:
bash
undefinedFind dSYM for binary UUID
根据二进制UUID查找dSYM
mdfind "com_apple_xcode_dsym_uuids == A1B2C3D4-E5F6-7890-ABCD-EF1234567890"
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
Or use a crash reporting service that handles symbolication (Crashlytics, Sentry, etc.).atos -arch arm64 -o MyApp.app.dSYM/Contents/Resources/DWARF/MyApp -l 0x100000000 0x105234567
或使用支持符号化的崩溃报告服务(如Crashlytics、Sentry等)。Part 5: MXBackgroundExitData
第五部分:MXBackgroundExitData
Track why your app was terminated in the background:
swift
@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
}追踪应用在后台被终止的原因:
swift
@available(iOS 14.0, *)
func analyzeBackgroundExits(_ data: MXBackgroundExitData) {
// 正常退出(用户关闭、系统回收)
let normal = data.cumulativeNormalAppExitCount
// 内存问题
let memoryLimit = data.cumulativeMemoryResourceLimitExitCount // 超出内存限制
let memoryPressure = data.cumulativeMemoryPressureExitCount // Jetsam(系统回收)
// 崩溃
let badAccess = data.cumulativeBadAccessExitCount // SIGSEGV
let illegalInstruction = data.cumulativeIllegalInstructionExitCount // SIGILL
let abnormal = data.cumulativeAbnormalExitCount // 其他崩溃
// 系统终止
let watchdog = data.cumulativeAppWatchdogExitCount // 过渡期间超时
let taskTimeout = data.cumulativeBackgroundTaskAssertionTimeoutExitCount // 后台任务超时
let cpuLimit = data.cumulativeCPUResourceLimitExitCount // 超出CPU配额
let lockedFile = data.cumulativeSuspendedWithLockedFileExitCount // 挂起时持有文件锁
}Exit Type Interpretation
退出类型解读
| Exit Type | Meaning | Action |
|---|---|---|
| Clean exit | None (expected) |
| Used too much memory | Reduce footprint |
| Jetsam (system reclaimed) | Reduce background memory to <50MB |
| SIGSEGV crash | Check null pointers, invalid memory |
| SIGILL crash | Check invalid function pointers |
| Other crash | Check crash diagnostics |
| Hung during transition | Reduce launch/background work |
| Didn't end background task | Call |
| Too much background CPU | Move to BGProcessingTask |
| Held file lock while suspended | Release locks before suspend |
| 退出类型 | 含义 | 操作建议 |
|---|---|---|
| 正常退出 | 无需操作(预期行为) |
| 内存使用过多 | 减少内存占用 |
| Jetsam(系统回收) | 将后台内存占用降至50MB以下 |
| SIGSEGV崩溃 | 检查空指针、无效内存 |
| SIGILL崩溃 | 检查无效函数指针 |
| 其他崩溃 | 查看崩溃诊断数据 |
| 过渡期间挂起 | 减少启动/后台工作负载 |
| 未结束后台任务 | 正确调用 |
| 后台CPU占用过高 | 迁移至BGProcessingTask |
| 挂起时持有文件锁 | 挂起前释放锁 |
Part 6: Integration Patterns
第六部分:集成模式
Upload to Analytics Service
上传到分析服务
swift
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()
}
}swift
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 {
// 加入重试队列
self.queueForRetry(jsonData)
}
}.resume()
}
}Combine with Crash Reporter
与崩溃报告工具结合
swift
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)
)
}
}
}
}
}swift
class HybridCrashReporter: MXMetricManagerSubscriber {
let crashlytics: Crashlytics // 或Sentry等
func didReceive(_ payloads: [MXDiagnosticPayload]) {
for payload in payloads {
// MetricKit能捕获传统报告工具可能遗漏的崩溃
// (例如:看门狗终止、内存压力退出)
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)
)
}
}
}
}
}Alert on Regressions
性能退化告警
swift
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")
}
}
}
}swift
class MetricsMonitor: MXMetricManagerSubscriber {
let thresholds = MetricThresholds(
launchTime: 2.0, // 秒
hangRate: 0.01, // 1%的会话
memoryPeak: 200 // MB
)
func didReceive(_ payloads: [MXMetricPayload]) {
for payload in payloads {
checkThresholds(payload)
}
}
private func checkThresholds(_ payload: MXMetricPayload) {
// 检查启动时间
if let launches = payload.applicationLaunchMetrics {
let p50 = calculateP50(launches.histogrammedTimeToFirstDraw)
if p50 > thresholds.launchTime {
sendAlert("启动时间退化:\(p50)s > \(thresholds.launchTime)s")
}
}
// 检查内存
if let memory = payload.memoryMetrics {
let peakMB = memory.peakMemoryUsage.converted(to: .megabytes).value
if peakMB > Double(thresholds.memoryPeak) {
sendAlert("内存峰值退化:\(peakMB)MB > \(thresholds.memoryPeak)MB")
}
}
}
}Part 7: Best Practices
第七部分:最佳实践
Do
建议
- Register subscriber early — In or App init
application(_:didFinishLaunchingWithOptions:) - Keep dSYM files — Required for symbolicating call stacks
- Upload payloads to server — Local processing loses data on uninstall
- Set up alerting — Detect regressions before users report them
- Test with simulated payloads — Xcode Debug menu in iOS 15+
- 尽早注册订阅者 — 在或App初始化时
application(_:didFinishLaunchingWithOptions:) - 保留dSYM文件 — 调用栈符号化必需
- 将负载上传到服务器 — 本地处理会在卸载时丢失数据
- 设置告警 — 在用户反馈前检测性能退化
- 使用模拟负载测试 — iOS 15+中使用Xcode Debug菜单
Don't
不建议
- Don't rely solely on MetricKit — 24-hour delay, requires user opt-in
- Don't ignore background exits — Jetsam and task timeouts affect UX
- Don't skip symbolication — Raw addresses are unusable
- Don't process on main thread — Payload processing can be expensive
- 不要仅依赖MetricKit — 24小时延迟、需要用户选择加入
- 不要忽略后台退出 — Jetsam和任务超时会影响用户体验
- 不要跳过符号化 — 原始地址无法使用
- 不要在主线程处理 — 负载处理可能消耗大量资源
Privacy Considerations
隐私注意事项
- MetricKit data is aggregated and anonymized
- Data only from users who opted into sharing analytics
- No personally identifiable information
- Safe to upload to your servers
- MetricKit数据是聚合且匿名的
- 仅来自选择共享分析数据的用户
- 不包含个人可识别信息
- 可安全上传至你的服务器
Part 8: MetricKit vs Xcode Organizer
第八部分:MetricKit vs Xcode Organizer
| Feature | MetricKit | Xcode Organizer |
|---|---|---|
| Data source | Devices running your app | App Store Connect aggregation |
| Delivery | Daily to your subscriber | On-demand in Xcode |
| Customization | Full access to raw data | Predefined views |
| Symbolication | You must symbolicate | Pre-symbolicated |
| Historical data | Only when subscriber active | Last 16 versions |
| Requires code | Yes | No |
Use both: Organizer for quick overview, MetricKit for custom analytics and alerting.
| 功能 | MetricKit | Xcode Organizer |
|---|---|---|
| 数据源 | 运行应用的设备 | App Store Connect聚合数据 |
| 推送方式 | 每日推送给订阅者 | Xcode中按需获取 |
| 自定义能力 | 完全访问原始数据 | 预定义视图 |
| 符号化 | 需自行处理 | 已预符号化 |
| 历史数据 | 仅订阅者活跃期间的数据 | 最近16个版本 |
| 是否需要代码 | 是 | 否 |
建议结合使用:Organizer用于快速概览,MetricKit用于自定义分析与告警。
Resources
资源
WWDC: 2019-417, 2020-10081, 2021-10087
Docs: /metrickit, /metrickit/mxmetricmanager, /metrickit/mxdiagnosticpayload
Skills: axiom-hang-diagnostics, axiom-performance-profiling, axiom-testflight-triage
WWDC:2019-417, 2020-10081, 2021-10087
文档:/metrickit, /metrickit/mxmetricmanager, /metrickit/mxdiagnosticpayload
相关技能:axiom-hang-diagnostics, axiom-performance-profiling, axiom-testflight-triage