写点什么

超越基准:采用基于指标的方法在真实设备上维持 iOS 长期的良好性能

作者:Vasuki Uday Kiran Vudathala
  • 2026-05-18
    北京
  • 本文字数:6269 字

    阅读完需:约 21 分钟

试想你有一个面向机组人员的移动应用:它没有备用服务器,在巡航高度没有 WiFi,并且应用在服务过程中出现崩溃无法通过简单重启来恢复,因为它以引导访问(guided access)模式运行,恢复需要完全重启设备而非简单重启应用。

机组成员执行的每笔事务(比如,餐饮订单、免税品销售、饮食偏好)都会写入设备并在飞机降落后与后端同步。库存通过蓝牙在机组设备间保持一致,任意时刻会选举一台设备为主设备。

我曾经是负责让该应用在 18 小时的飞行中保持可靠的核心性能团队成员。早期版本曾在现场令某位机组成员遭遇失败:屏幕冻结、用餐服务还在进行中、没有崩溃日志、无法恢复。正是那次事故促成了我在此处描述的方法论的诞生。

一个应用可以通过所有基准测试(比如,冷启动低于 2 秒、API 延迟低于 400ms、十次测试无崩溃),但在真实使用四小时后仍可能出现降级且易崩溃的体验。本文记录了这种失效模式,解释为何系统性的理念会被忽视,并描述用于检测与预防它的架构方法以及 Xcode Instruments 分析技术。

通过基准效能测试所造成的误解

移动性能工程中常见的模式是基于孤立的测量来标记应用“性能良好”,例如,Screen X 在 320ms 内渲染、API Y 在 400ms 内响应、冷启动 1.8 秒。只要仪表盘变绿,应用就能发布。但在机组 18 小时飞行的第 6 小时,应用可能会面临冻结的风险。

这种模式属于时点采样(point-in-time sampling),也是最常见的机制,即团队发布的应用在真实使用中会降级。用户会浏览、滚动、后台、恢复、切换上下文,并在远远超过基准窗口的会话中反复使用。会话期间的性能是由 CPU 负载、内存状态、热调节、操作系统调度与后台进程竞争共同塑造的动态系统行为,这些在一小时的基准测试中是无法完全暴露的。

相关研究也支持这种模式:谷歌的移动性能研究发现,当加载时间超过 3 秒时,53%的移动访问会被放弃,这一结论影响了业界对性能的认知。但是,该研究仅关注初始加载,测量的是用户是否决定留下的瞬间,而非随后数小时内发生的状况。对于在持续使用环境中运行的原生应用来说,这种表述完全错过了我们所述的失效模式。用户对性能的敏感是确凿且有证据的,但在长会话应用中,降级是累积性的,并非在首次加载时就显现。它在数小时内悄然累积,直到无法忽视。

为什么针对真实设备的测试是不可妥协的

模拟器在功能测试中有其合理的用途,但在性能测试中则是不可信的。最直接影响用户感知性能的系统行为在模拟环境中要么被抽象掉了,要么根本不存在,包括:

  • 热限流(Thermal throttling):现代 SoC 在持续 CPU 负载下会实行激进的频率缩放,这在模拟器上从来不会发生。

  • 并发进程带来的内存压力:真实设备要运行后台服务、推送守护进程、定位服务和竞争应用,操作系统的内存管理子系统无法在沙箱中复现。

  • 操作系统级的生命周期控制:应用后台化、内存警告

(UIApplicationDidReceiveMemoryWarningNotification)和前台恢复由基于实时使用的 OS 启发式(heuristics)方法来触发。

  • 电池消耗动力学:功耗是依赖于硬件、无线电状态和热调节的物理现象。

近期的行业证据

Meta Threads iOS(2024 年 12 月):Meta 工程团队发现,即使微小的导航延迟注入也会导致用户阅读更少的帖子并减少发帖。这种延迟只能通过在真实设备上的会话级插桩来测得。

Instagram Android 后台过热(2025 年 5 月):谷歌确认 Instagram 应用中的后台进程缺陷导致 Android 设备过度耗电并发热。该缺陷在持续后台条件下分析时才可见,这恰好是模拟器测试无法复现的场景。

跨指标放大:核心见解

性能工程领域的一个关键观点是,指标不是孤立失败的,它们是作为相互关联的系统行为而失败的。

当 CPU 过热时,热限流会降低时钟频率,FPS 下降,主线程队列积压,用户会看到界面冻结。当内存泄漏累积时,堆增长最终会使系统在压力下回收内存时触发 jetsam 终止。性能测试人员看到了崩溃,性能工程师回溯到会话的第 1 小时并找到启动连锁反应的内存泄漏。

图 1:跨指标放大——指标在因果链中失败,而非孤立地失败。

下面四条链是在生产环境的 iOS 应用中观察到的最重要模式。每一条都在真实生产中出现过:

这表明生产环境中单一退化的指标只是因果链的终点,而非根本原因。第 3 小时的崩溃率上升并不意味着稳定性本身有缺陷,而是从第 1 小时就开始累积的内存压力的结果。请始终在 Xcode Instruments 中沿相同时间轴关联信号。

iOS 性能指标分类法

成熟的性能策略是一个关于指标如何相互作用的因果模型,而不是一份要跟踪的指标清单。下表映射了每个信号揭示的内容以及其退化时触发的后果:

*表示在大多数 iOS 工程项目中系统性欠量化的指标。

在 Xcode Instruments 中剖析每个指标

上述分类中的每个指标在 Xcode Instruments 中都有直接的一方(first-party)插桩路径。本节将提供剖析演练,并注记在真实设备会话测试中应关注的关键点。

在开始任何会话分析前,请确认我们的设置:所有性能剖析必须在物理设备上进行。连接设备,选择其为运行目标,然后导航到 Product → Profile (⌘I)并选择合适的模板。切勿在模拟器上做性能剖析。

热态:Time Profiler + Activity Monitor 模板

Instruments 模板:Time Profiler + Activity Monitor(含 Thermal State track)

热行为是长会话退化的早期指标之一。Time Profiler 配合 Activity Monitor 模板能够暴露热态转换(Nominal → Fair → Serious → Critical)与 CPU 活动的并列视图,从而可以直接把持续 CPU 负载与热升级关联起来。

温度本身并不重要,关键在于热限流相对于 CPU 峰值和 UI 退化是何时开始的。在中端设备上,持续 CPU 使用率高于约 50%通常会在几分钟内触发限流。一旦设备进入“serious”热态,时钟频率就会降低,随之就会发生 FPS 退化与主线程争用等下游效应。

关键观点:热态转换是先行指标。当 FPS 下降时,其根本原因往往在热时间线的更早位置就能看出端倪。

图 2:Time Profiler + 热态。压力性任务的持续 CPU 负载驱动热态从 Nominal(绿色)升级为 Fair。热态转换是先行指标:一旦达到 Fair,负载会进一步推向 Serious 并触发时钟频率下降和下游 FPS 退化。

内存泄漏与占用:Leaks 模板

Instruments 模板:Leaks(包含 Allocations + Leak Checker)

随着时间推移,内存行为会决定应用是否能在会话中保持稳定。Allocations 工具能够揭示内存使用是稳定还是持续增长。

一个健康的应用在初次加载后会达到平台状态。持续上升的内存曲线表示泄漏累积,这通常是由未释放的 view controller、无驱逐策略的缓存或无意中保留的对象所导致的。

关键门槛

  • 30 MB/小时的持续增长 → 需要调查

  • 跨导航周期持续增长的对象 → 可能存在泄漏

关键观点:内存泄漏很少导致立即失败。它们会悄然累积,稍后以崩溃、暖启动退化或 UI 不稳定的形式显现。

图 3:Allocations + Leaks。会话期间 Generation 快照 A→B 中的持久对象和堆增长(223→436 个对象,1.17GiB→2.05GiB),泄漏检查确认会话中存在活动泄漏。来自导航周期的未回收堆在 Generation C 触发部分 GC 扫描。

FPS 与丢帧:Hitches 模板

Instruments 模板:Hitches(包含 Display、Time Profiler、Thermal State 和 Hangs track)

帧率是最接近用户感知性能的代理指标。Hitches 工具同时暴露卡顿持续时间与卡顿类型,比如,Expensive Commit(s)、Expensive GPU 或 Commit to Render 延迟,这允许工程师精准定位渲染流水线的哪个阶段导致了丢帧。

在活跃使用期间持续低于 45 FPS 就会被用户感知到,应被视为缺陷。更重要的是,丢帧必须与 CPU 峰值、内存压力或主线程阻塞等上游信号关联。

苹果定义了以下卡顿率阈值指南:

  • < 5 ms/s 卡顿率 → 可接受

  • >10 ms/s → 用户可感知的降级

  • FPS < 45 → 需立即采取行动

把这些纳入 CI 通过/失败的判断标准。

关键观点:丢帧很少只是渲染问题。更常见的是上游争用的症状。

图 4:Hitches 工具。会话开始时检测到两次帧卡顿,包括一次高严重度的 Expensive Commit(s)卡顿 16.67ms,超出了 5ms 的可接受延迟阈值。Display 1 中的 CPU 活动与 VSync 错位确认主线程渲染压力为根本原因。

主线程阻塞:Time Profiler 模板

Instruments 模板:Time Profiler

主线程决定了 UI 的响应性。任何阻塞性的工作,包括 JSON 解析、数据库访问和图像解码,在主线程上同步执行时都会直接转化为用户可见的卡顿。

Time Profiler 能够暴露阻塞时段及其调用栈的来源。即便是看上去很快的操作(比如,图像解码),在内存或热压力下,同步在主线程执行也可能导致数秒级的严重挂起。

关键阈值

  • 16 ms → 超出帧预算

  • 50 ms → 可察觉的滞后

  • 500 ms → 被 watchdog 终止的风险

关键观点:主线程阻塞是多个性能问题对用户可见的汇聚点。

图 5:Time Profiler。主线程持续接近满载,UIKit 布局与 SwiftUI 渲染在单次更新序列中消耗了 683ms。热态从 Nominal 升级到 Fair,确认了持续主线程负载的下游影响。

暖启动延迟:App Launch 模板 + os_signpost

Instruments 模板:App Launch(+ os_signpost 自定义标记)

暖启动延迟比冷启动更能反映真实的使用模式。它衡量了应用在后台之后恢复状态的效率。

在重复前台周期中,如果出现退化是潜在问题的强烈信号,例如,内存压力、低效的状态恢复或不必要的网络依赖。

在四个点上使用 os_signpost 标记暖启动,分别是applicationWillEnterForeground、根视图控制器的viewWillAppear、第一个数据就绪的回调,以及布局后的viewDidAppear

import oslet log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "WarmStart")// At willEnterForeground:os_signpost(.begin, log: log, name: "WarmStart")// At viewDidAppear:os_signpost(.end, log: log, name: "WarmStart")
复制代码

关键阈值:

  • < 800 ms → 健康

  • 800–1,500 ms → 需要调查

  • >1,500 ms → 需要行动

  • 跨会话增长 20% → 可能需要回归

关键观点:暖启动延迟通常是会话级退化的最早的可测量指标。这里的回归几乎总能比崩溃率回归早 1-2 个版本。

图 6:os_signpost 摘要。App Launch 模板会捕获 WarmStartApp 的暖启动恢复周期。它记录了两个前台恢复事件,分别为 450ms 和 632ms,接近 800ms 的调查阈值。系统恢复类别包括NSProcessInfoInteractionTracking,其峰值为 895ms,确认了重复前台周期中的累积延迟增长。

单独看这些指标能提供的见解很有限。它们的价值在于会话时间线上的关联分析,热态变化会先于丢帧,内存增长会先于崩溃,延迟会在多层之间放大。我们的目标不是在某一时间点测量性能,而是追踪其在持续真实条件下如何演变并导致最终的失败。

案例研究 A:航空机组应用——从预生产到可飞行

这是与一家大型国际航空公司的匿名合作。iOS 应用支持机上的本地点餐、实时菜单更新、饮食偏好管理与机组协调。这样的要求使普通的性能标准难以胜任。设备只能通过蓝牙进行点对点网状通信;任意时刻都要有一台设备担任主设备以确保库存在机组间保持一致;在 35000 英尺没有 WiFi;销售丢失就无法恢复;应用必须可靠运行 18 小时,能在后台、强制退出与恢复周期中存活,并且绝不能丢失一条记录。

为了理解其重要性,考虑如下的运营环境:18 小时超长航线(例如,新加坡—纽约、悉尼—达拉斯)是任何移动应用面临的最苛刻的持续使用场景。设备在登机、送餐、免税与机组协调期间始终处于激活状态,没有机会进行重启或恢复。

短时间的测试遗漏了哪些情况

初始验证使用旗舰设备进行了 30–60 分钟的会话。所有的 KPI 均能通过:冷启动 1.4s、中位 API 响应 310ms、稳定的 60 FPS、十次运行均无崩溃。随后,基于 Firebase Crashlytics 和 Dynatrace RUM 数据建立设备矩阵并启动了扩展的 8 小时会话测试。

在 8 小时协议中的退化

指标通过 Xcode Instruments(iOS)获取。

根本原因与修复措施

  • 导航栈内存泄漏:在 80–120 次导航事件间,没有回收堆上的 380–450MB 内存。通过视图控制器释放审计(view controller dealloc audit)与 LRU 镜像缓存,我们修复了这个问题。T+8 小时内存:638 MB → 142 MB。

  • 主线程图像解码:PNG 图像在主线程上进行同步解码,导致 T+4 小时约 4.6 秒的严重挂起。通过使用 DispatchQueue.global()将图像解码移至后台队列修复。FPS 在 T+8 小时稳定在 56+ fps。

  • 固定间隔的后台轮询:8 小时内发送了 480+次不必要的请求。我们通过热适应轮询进行了修复。T+4 小时设备温度:41°C(低于 51°C)。

  • 并发负载暴露:在设备会话测试同时进行的 JMeter 负载测试显示,500 个并发用户下 API 的 p95 延迟为 2240ms,后端瓶颈在高峰使用时加剧了设备端的延迟。对后端 API 服务器和 CDN 缓存进行连接池调优后,我们解决了该问题,使高峰期的 p95 延迟降至 480ms。

结果验证了该方法论的有效性:应用在生产部署的前 90 天内未记录任何性能事件。

案例研究 B:零售应用中延迟引发的 UI 退化

后端基础设施的迁移在 SLA 范围内的产品列表端点引入了额外 300ms 的 API 延迟,APM 工具将其标记为微小的变化。但是,对真实设备的会话级测试揭示了级联效应:

  • 响应负载处理在额外等待窗口期间在主线程上执行。

  • 主线程利用率在处理响应的滚动过程中超过了帧预算的阈值。

  • 在产品浏览会话中,FPS 从 58 降至 38–42,这些会话的转化率恰好是最高的。

  • 这项退化在任何单一事务跟踪中都没有出现,仅在 30–60 分钟的模拟浏览会话中出现了。

换句话说,一个 300ms 的后端更改在价值最高的用户流程中造成了 35%的 FPS 后果,因为放大链条从未被建模跟踪。Meta Threads 在 2024 年 12 月独立记录了相同的模式。

生产级 iOS 应用的参考阈值

在持续会话条件下的最低可接受标准为:

架构建议

1.将会话持续时间定义为架构要求

记录应用的最长会话持续时间,而非平均值,并将其纳入性能需求文档,而非测试计划。对于 18 小时的航线,需要通过至少 8–12 小时的设备测试进行验证。

2.从第一天起就启用热态跟踪

将带有 Thermal State track 的 Time Profiler 添加到每周的设备测试中。从第一个 Sprint 开始,Thermal State track 必须处于激活和记录状态,如果在生产事故后再进行热响应策略的追溯性修复,那么其成本要远高于从一开始就构建它们。

3.将负载生成整合到每个性能测试周期中

针对最低负载的客户端评估会产生乐观的结果。每个 Sprint 级的评估应该将 Xcode Instruments 与在峰值并发用户数下的 JMeter 或 LoadRunner 场景进行匹配。

4.根据 RUM 而非直觉构建设备矩阵

从 Firebase Crashlytics 和 App Store Connect 中提取顶级设备型号。按会话数和崩溃率排序。重要的设备是用户实际使用的设备。

5.将暖启动添加到 CI 性能仪表板

从今天开始就添加 os_signpost 标记。将暖启动延迟作为主要的 CI 指标进行配置。在暖启动的回归中,任何高于基线 15%的增长都应阻止发布。

6.将热预算阈值定义为通过/失败的标准

对于每个支持的设备级别,指定 T+4 小时和 T+8 小时的最大允许温度。在会话测试中超过 T+4 小时阈值的应用不得进入生产。

结论:性能是系统属性,而非单一指标

在实际操作中,iOS 性能工程常常默认把性能视为组件的属性,比如,这个屏幕渲染快、那个 API 响应迅速、还有这个动画效果流畅。这种表述方式导致的结果是即便有了绿灯的仪表盘,依然交付了退化的用户体验。

生产环境中的性能是由应用代码、设备硬件、操作系统资源管理、网络条件与用户行为模式随时间交互的一个突现行为。它无法在单一时点、单一指标或模拟器上进行测量。

本文中的剖析演练为每位从业者提供了通过 Xcode Instruments 捕获分类法中每个信号的直接的、第一方(first-party)的路径。因果链模型为连接这些信号以进行根本原因分析提供了框架。案例研究 A 展示了当这些实践在 18 小时会话协议的系统性应用时会发生什么,而案例研究 B 则说明了当放大链条未被建模时会产生什么样的代价。

性能不是在发布前检查的一个特性。它是内置于架构中的基本系统属性,我们需要通过 Instruments 进行测量,并通过崩溃报告和真实用户监控在生产中进行监测。

查看英文原文:Beyond the Benchmark: A Metrics-Driven Approach to Sustained iOS Performance on Real Devices