夜雨聆风学习资料网

ARTICLE · 1239014

iOS从性能指标到线上诊断

iOS从性能指标到线上诊断

MetricKit 学习:从性能指标到线上诊断

学习入口:Apple Developer Forums 的 MetricKit 标签页。本文结合代表性讨论、Apple API 文档和 WWDC26 资料整理,资料核对日期为 2026 年 10 月 23 日。文中的接入建议和房间业务案例属于学习推演,未检查或改动实际业务工程。

线上排查经常遇到这样的情况:用户反馈应用卡顿、发热或被系统退出,研发却没有拿到足够的崩溃堆栈。即使已有崩溃平台,也还需要回答一些性能问题:哪个版本启动变慢了?卡顿集中在哪种使用状态?后台退出是否增加?

MetricKit 提供系统采集的性能指标和诊断报告,可以为这些问题补充证据。阅读论坛时,最容易忽略的是时间背景:较早的讨论主要围绕 MXMetricManager,而 Apple 在 WWDC26 介绍了面向 iOS 27 的新 MetricManager API。接入方式、数据结构和诊断范围都需要按版本理解。Apple 官方介绍

先分清指标和诊断各自能回答什么。

日常关注“这个版本的表现有没有变差”,适合看聚合指标;追查“出现了哪类异常、相关代码路径是什么”,则需要诊断报告。两类数据可以相互补充,但统计粒度不同。

数据类型
旧 API 中的主要载体
新 API 中的主要载体
适合回答的问题
指标
MXMetricPayloadMetricReport
启动、响应和资源消耗是否发生变化
诊断
MXDiagnosticPayload
,内部可包含诊断数组
DiagnosticReport
,描述单个诊断事件
发生了哪类问题,有什么调用栈和环境信息

Apple 对交付节奏也作了区分:指标按系统安排汇总,通常覆盖此前约 24 小时;诊断在可用时交付,iOS 15 起采用即时交付机制。“即时”描述的是诊断交付机制,不能据此承诺故障发生后服务端一定在几秒内收到数据。应用执行机会、本地处理和上传网络仍影响最终可见时间。MetricKit 概览

尤其对于应用已被终止的情况,不能假定死掉的进程还能执行回调和上传。工程上应把系统报告交付、客户端接收和服务端入库分别记录;也不能把指标汇总当成逐次会话流水。

论坛答复需要同时看日期、身份和后续变化。

这次学习中,有几组讨论特别适合放在一起读:

讨论
能确认的内容
阅读时保留的边界
Jetsam 是否提供诊断
2026 年初 DTS 答复旧接口提供相关退出计数,尚无对应诊断详情
后续新 API 已增加内存异常诊断,旧答复不能覆盖所有版本
新 MetricKit 如何符号化
DTS 建议继续优先在设备外符号化
需要保留匹配二进制的符号资料
模拟报告菜单缺失
工程需要链接 MetricKit,并通过 Xcode 运行;帖子随后确认了菜单位置
菜单出现与产生真实业务数据是两件事
部分系统收不到 Crash、Hang 回调
发帖者描述了复现环境,Apple 请求最小工程及反馈单
这段讨论尚不足以证明所有这些系统版本都不支持该能力
CPU 诊断时间字段如何解释
开发者提出了报告区间和采样时长不一致的疑问
阅读时未见官方答复,不能把问题中的假设写成结论

论坛最有用的部分,是暴露真实工程里容易误解的边界。API 是否支持、字段语义是什么,仍应回到对应版本的官方文档核实。

新旧 API 的变化,影响的不只是调用语法。

旧接口通过 MXMetricManager.shared 管理订阅,由实现 MXMetricManagerSubscriber 的对象接收回调。官方还说明,一些指标来自不同系统来源,因此一天可能收到多个 payload;“每日汇总”不能简单实现成“每天只接受一条”。旧管理器文档

iOS 27 起,新接口使用可以实例化的 MetricManager,通过 metricReports 和 diagnosticReports 两条异步序列交付报告。它们可以用 for await 消费,报告支持 Codable。Apple 要求按需要保持管理器存活,并提醒并发消费同一报告序列可能使消费者收到不确定的报告子集。新管理器文档

我会据此把接入组织成一个长期存活的采集服务:每条序列设置一个消费入口,再在入口内部向本地存储、上传和分析模块分发。这样可以明确任务的创建、取消与恢复责任,也能避免业务页面各自创建管理器、重复订阅。

下面仅展示新 API 的指标消费结构。需要支持该接口的 SDK 和 iOS 27;诊断流应由另一个长期任务独立消费。persist 和 reportError 是调用方提供的处理逻辑。这段示例未在实际工程编译。

import Foundation
import
 MetricKit

@available
(iOS 27.0, *)
func
 receiveMetricReports(
    from
 manager: MetricManager,
    persist
: (Data) async throws -> Void,
    reportError
: (Error) -> Void
) async {
    for
 await report in manager.metricReports {
        do
 {
            let
 data = try JSONEncoder().encode(report)
            try
 await persist(data)
        } catch {
            reportError(error)
        }
    }
}

这里把写入失败显式交给错误处理方。实际接入还需要设计有界重试、失败记录和恢复策略;仅记录一条错误后丢弃报告,无法满足可靠采集要求。支持较早系统的工程,则需要保留对应版本的旧接口实现,按系统可用性选择接入路径。

“没有内存诊断”必须结合 API 版本解释。

2026 年初的 Jetsam 讨论中,DTS 指出可以通过 MXAppExitMetric 相关的 cumulativeMemoryResourceLimitExitCount 观察内存资源限制退出,但当时没有对应的诊断详情。DTS 答复

当前官方文档已经列出 MemoryExceptionDiagnostic:当应用或扩展因超过内存限制而终止时生成诊断,并提供调用栈树。因此,学习旧文章时,应补上“旧接口下的能力边界”这一前提。内存异常诊断文档

同时,“超过自身内存限制而终止”有明确条件,不能扩写成“所有内存压力退出都有完整诊断”。如果拿到的只有退出计数,它能说明某一类退出增加,却不能直接定位到某个页面、对象或泄漏点。接下来仍需结合设备档位、使用时长及本地内存分析验证假设。

CPU 诊断也需要分类理解。MXCPUExceptionDiagnostic 与新的 CPUExceptionDiagnostic 都可能描述 fatal 或 nonfatal CPU 异常,收到这类诊断本身不能证明应用已经崩溃。旧 CPU 诊断、新 CPU 诊断

处理数据时,先保留三种时间。

CPU 时间讨论中,发帖者遇到报告起止时间相同、采样时长却超过两分钟的情况,并尝试反推采样窗口。这个问题提醒我们:不同字段描述的对象可能不同,不能只靠数值做时间对齐。论坛问题

官方把旧诊断 payload 的 timeStampBegin、timeStampEnd 定义为报告周期的起止时间;新的 DiagnosticReport.timeRange 也是报告覆盖区间。totalSampledTime 则表示 CPU 异常期间应用被采样的总时长。这些定义本身没有给出“用报告终点减去采样时长,就能还原精确业务窗口”的保证。旧诊断 payload、新报告时间区间、采样时长

我会在采集系统中保留下面三种时间语义,并让分析人员明确区分:

时间语义
来源
用途
报告覆盖区间或事件时间信息
MetricKit 原始报告
判断数据对应的使用阶段
客户端接收时间
采集服务
衡量交付延迟,辅助排查接入问题
服务端接收时间
入库服务
衡量上传延迟和数据新鲜度

如果要关联服务端请求或远程配置变更,可以先用报告区间筛选候选事件,再用独立日志和状态证据缩小范围。缺少精确时间依据时,应把关联结果作为待验证假设。

聚合报告不能附会成某一次启动的状态。

论坛有人询问:一个跨越多次运行的指标 payload,怎样与会变化的进程和低电量模式信息对应?帖中并未形成可依赖的精确对应契约,其他开发者也明确表达了自己的假设。指标 payload 讨论

对此,我会遵守一个数据处理原则:报告到达时读取到的当前页面、实验分组和系统状态,只能说明接收时刻,不能直接覆盖到整段历史使用。需要历史状态,就在状态发生变化时记录,或者采用具备明确归因能力的接口。

版本也有类似问题。旧 MXMetricPayload 的 includesMultipleApplicationVersions 表明报告周期内版本是否发生过变化,而 latestApplicationVersion 指周期结束时的版本。对于包含多个版本的报告,不宜把全部性能结果直接归给最后一个版本。指标 payload 文档

直方图需要按样本聚合,不能平均设备的 P95。

MetricKit 的部分指标使用直方图表达:每个桶有上下界和观测数量;另一些指标是标量或平均统计,并带有单位。保留这些结构,才能正确解释结果。官方数据分析指南

假设设备 A 有 10 次启动,设备 B 有 1,000 次启动。若各自算一个 P95 再取平均,两台设备得到相同权重,结果无法代表 1,010 次启动的整体分布。更合理的做法是,在统计口径和单位可比时合并桶计数,再寻找累计样本达到目标比例的位置。

这种方法得到的分位数仍受桶宽限制。如果目标落在 1 至 2 秒的桶里,只能确认它处于该区间;采用插值还需要声明额外假设。桶边界不一致时,也不能按数组下标直接相加,应先确定合并策略。

比率类指标同样要明确分母。下面是自拟计算,用来说明使用时长不同会影响权重:

设备 A:卡住 3 秒,前台使用 30 分钟
设备 B:卡住 3 秒,前台使用 5 分钟
合并后:6 秒 / 35 分钟,约为 10.29 秒/小时

如果分别算成 6 秒/小时和 36 秒/小时,再简单平均,会得到 21 秒/小时。统计报表应写清自己表达的是按使用时长加权的总体表现,还是每设备等权的体验分布;这两者服务于不同分析目标。

状态归因能缩小排查范围,也有覆盖范围。

新 MetricKit 可以与 StateReporting 配合,将稳定的业务阶段或配置状态带入性能分析。同一 domain 同时只有一个活动状态,不同 domain 可以独立表达状态。Apple 建议使用有业务含义的阶段,并避免过量、过细的状态。WWDC26 介绍

例如,一个房间业务可以用不同状态域描述“当前使用区域”和“动效方案”:前者区分房间、成员列表等阶段,后者区分新旧方案。标签和值是应用自己设计的分析维度。账号、房间唯一标识和每次点击生成的随机值,会让状态数量迅速增长,也难以形成稳定的对比人群。

当前文档还列出了两个具体边界:MetricKit 使用 stable metadata,volatile metadata 不会出现在其报告中;状态数量超过系统限制后,部分状态数据会缺失,并通过 hasExceededStateLimit 标记。监控与状态配置文档

更容易误读的是指标范围。当前 MetricReport 文档说明,卡住时间、动画卡顿时间、终止计数、signpost 区间和运行时长等部分指标可以进入 stateEntries;CPU、内存、网络、磁盘 I/O、GPU、启动及磁盘空间等指标只出现在 intervalEntries。因此,注册一个房间状态,并不会自动得到全部资源指标按房间拆分的结果。MetricReport 文档

如果关心“某段资源加载消耗了多少 CPU、内存和写入”,可以进一步研究 MetricKit 的自定义 signpost 区间。Apple 示例也特别区分了 log handle 和 signpost API 的作用:使用普通 signpost 并不意味着一定填充 MetricKit 的这些资源测量属性。官方接入示例

诊断报告要经过符号化,报告数量也要保留采样语义。

新旧诊断都可能包含调用栈树。针对新 MetricKit 的论坛讨论中,DTS 建议继续优先在设备外符号化;仅为了符号化,无需引入更复杂的 CrashReportExtension。DTS 符号化建议

据此,采集端应保留原始调用栈及二进制标识,发布端应归档能够匹配的应用与依赖符号。单凭可读版本号选择 dSYM 容易出错,处理时需要核验 Mach-O UUID 等匹配信息。符号化失败也应成为可观测状态,避免报告已经入库,却长期只剩地址可看。

此外,Apple 明确说明,MetricKit 不为每一次诊断事件生成报告,例如 hang 诊断受设备检测配置或采样条件影响。因此,“收到了 100 份 hang 诊断”可以成为排查证据,却不足以直接推算全体用户的卡顿率。DiagnosticReport 文档

验证接入,要分别检查模拟数据和真实设备数据。

Apple 官方示例要求在真实设备上验证报告交付。当前模拟入口是 Debug > MetricKit > Simulate MetricKit Payloads;论坛中 Apple 工程师进一步说明,应用需要链接 MetricKit 并由 Xcode 运行,按钮才会显示。发帖者随后确认了这些条件和菜单位置。示例要求、菜单问题答复

模拟报告是样例数据,适合验证解析、编码、本地存储和上传处理。它不能证明刚刚执行的某段业务代码已经被系统测量,更不能代替真实设备的报告验证。模拟报告说明

还有一类问题来自工具链。论坛中曾出现用 Xcode 26.4 构建后,在较早系统启动时找不到 mxSignpost 符号的情况。Xcode 26.4.1 的官方发布说明确认了修复,并要求用更新后的 Xcode 重新构建。这说明接入验证需要覆盖“构建工具版本与运行系统”的组合。论坛讨论、Xcode 26.4.1 发布说明

把这些认识用到一次实际排查中。

假设新版本上线房间动效后,用户开始反馈卡顿。下面是我会采用的排查顺序,属于工程推演:

第一步先确认比较对象可比:同类设备、相近使用时长、相同指标口径,避免用户结构变化干扰版本判断。然后检查报告覆盖区间,防止把延迟上报的旧问题归给刚启用的功能。

如果状态数据表明问题集中在新动效阶段,就沿相关诊断和本地复现继续调查。如果只有整日内存峰值升高,还不能直接断定动效就是原因,需要补充区间测量或 Instruments 证据。修复后也应采用相同口径比较,保留样本数量和缺失情况。

第一次接入时,我会把验收范围限制在以下结果:

  1. 1. 明确目标系统对应的新旧 API 路径,采集对象和任务生命周期可追踪。
  2. 2. 模拟报告能够编码、落盘、上传和回读,失败能留下可排查记录。
  3. 3. 真实设备报告能够保留原始时间和环境信息,并区分来源及报告类型。
  4. 4. 至少一份可用诊断能够匹配符号资料,处理平台能够展示失败原因。
  5. 5. 选择一个稳定业务状态验证归因,确认所需指标确实支持该维度。
  6. 6. 报表标明单位、样本、观察窗口和数据新鲜度,避免用诊断份数冒充故障率。

等这些结果得到验证,再扩大状态覆盖和分析维度。这样每增加一类数据,都能说清它支持什么判断,以及还需要什么证据。

相关学习资料