返回文章列表

iOS 性能优化 系列 07|一次真实的 iOS 性能排查,是怎样收敛到根因的

真正难的不是打开 Instruments,而是先判断自己看到的到底是不是同一个问题,再把一堆伪线索一层层排掉

做 iOS 性能排查,最烦的不是工具难用,而是现场经常一开始就是乱的。

产品说首页卡,测试说是列表掉帧,开发觉得可能是图片,后端又怀疑接口慢。
每个人说的都像是一个问题,但真开始看时,往往会发现大家描述的根本不是同一件事。

这类活我做过几次之后,最大的感受不是“哪个工具最重要”,而是:
性能排查首先是收敛问题,其次才是分析数据。

如果问题没有先收敛,工具开得越多,越容易把自己带偏。
因为你看到的每个指标都像有信息,但每个信息都不够构成结论。

下面这篇文章,不讲“理论上怎么排查”,而是按一次真实项目里会发生的节奏往下写:
一个首页信息流掉帧的问题,是怎么一步一步从“感觉有点卡”,收敛到可以动手改的。

先把场景钉死,不然后面所有分析都会飘

那次问题最开始报上来的描述很普通:

  • 首页滑动不顺
  • 某些机型更明显
  • 最近版本感觉更卡了

这种描述听起来像有信息,其实几乎不能直接拿来排查。
因为它至少混了三层东西:

  • 页面范围不清楚
  • 现象类型不清楚
  • 复现条件不清楚

真正能开始工作的描述,最后被压成了这样:

iPhone 13 Pro,iOS 18,登录态账号 A,冷启动进入首页推荐流,首屏加载完成后切到“关注”tab,再切回推荐流,连续快速上滑 4 屏,从第 3 屏开始持续掉帧,问题主要出现在图文混排和视频卡片混排区。

这段话的重要性,不在于它写得像测试用例,而在于它一下子把很多歧义砍掉了:

  • 不是全 App 卡,是首页推荐流卡
  • 不是首次进入卡,是第二次进入更明显
  • 不是点击慢,是滚动掉帧
  • 不是所有区域都一样,是图文混排区更明显

排查真正开始,往往就是从这种“把范围钉死”开始的。
没有这一步,后面所有“分析”都可能只是在追自己的想象。

先别急着看工具,先判断这到底是偶发卡顿还是持续低帧

“滑动不顺”这四个字,实际可能对应完全不同的问题。

那次我最先做的,不是开 Instruments,而是先录屏、反复滑几次,把现象定性。

最后定下来,这不是那种“偶尔抖一下”的卡,而是很典型的持续低帧:

  • 一旦进入特定内容区,连续几屏都不顺
  • 不是某一帧突然爆炸,而是整个滚动阶段都发沉
  • 用户主观感受不是“突然卡了一下”,而是“这段一直滑不动”

这个区分非常重要。

因为偶发卡顿通常更像:

  • 某次图片解码突然砸在主线程
  • 某个大对象初始化时机不对
  • 某个 layout pass 偶尔变重

而持续低帧更像:

  • 每帧都在做重复工作
  • cell 结构本身太重
  • 滚动期间不断有异步结果回填
  • 列表状态刷新范围过大

如果一开始没把这两类问题分开,后面看任何数据都容易误读。

复现路径一定要写下来,不然“改好了”这种话没有任何意义

性能问题最容易出现的假象之一,就是“刚才还挺明显,现在怎么又没了”。

所以那次排查里,我先做的一个动作很笨,但非常值钱:把复现路径写成固定步骤。

当时整理出来的是:

  1. 杀掉 App,重新冷启动
  2. 登录账号 A
  3. 进入首页推荐流,等待首屏完全稳定
  4. 切换到“关注”tab
  5. 再切回推荐流
  6. 快速连续上滑 4 屏
  7. 观察第 3 屏开始的帧率表现和主线程占用

同时把环境也记死:

  • 设备型号
  • 系统版本
  • 网络条件
  • 数据量级
  • 是否打开调试开关

这件事的意义不只是方便别人复现,更重要的是方便后面验证。

因为性能优化里最不值钱的一句话就是:

我感觉比刚才顺了。

没有固定路径,“顺了”这两个字没有分析价值。
它可能只是因为数据不一样、网络不一样、页面没滑到同一段,甚至只是这次手指没滑那么快。

第一轮判断,不找根因,只判断问题落在哪条链路上

很多人一开始就想问:

  • 到底是哪一行代码慢
  • 到底哪个函数最耗时
  • 到底是哪个库的问题

但真实排查里,这样问通常太早。

那次我先做的,不是抓“最终元凶”,而是粗分链路:

是数据问题,还是渲染问题

这一步很关键,因为首页慢很容易让人本能怀疑接口。

我先做了一个非常直接的验证:
尽量固定网络结果,让第二次进入推荐流时数据尽量来自已有结果,而不是网络变动。

结果很明确:
数据已经回来了,滚动照样沉,而且是稳定沉。

这一步基本就能把“接口慢导致滚动卡”从主问题里排掉。

接口慢会影响首屏时间,但它不太解释“第二次进入后、某段内容开始持续低帧”。

是主线程重,还是后台工作不断打回主线程

接下来看的就不是“卡不卡”,而是卡的时候主线程在干什么。

这一步的目标也不是一上来就找到哪一帧,而是先判断问题形态:

  • 是主线程持续偏忙
  • 还是后台回调太密,主线程被一波一波打断

最后看到的情况更偏前者:
主线程并不是偶尔炸一下,而是在滚动阶段一直偏重。

这就说明方向开始变清楚了:

  • 重点不在网络
  • 重点不在单次大任务
  • 重点更像列表展示链路本身太重

是单点异常,还是系统性过载

这是我每次都很看重的一个判断。

如果只有一个函数偶发跑出 200ms,那很好办,抓它就行。
但那次不是这种问题。

那次真正麻烦的地方在于,没有哪个点离谱到“一眼真凶”,而是很多本来不算夸张的小开销叠在一起,全部落到了滚动关键路径上。

这种问题最容易让人烦躁,因为它不像 crash 有一条特别清晰的栈。
它更像系统在告诉你:你过去每一个“先这么写着”的小决定,现在开始一起收费了。

到这一步才值得开 Instruments,而且一定是带着怀疑去看

我一直不太喜欢那种“先全开工具再说”的排查方式。
不是工具没用,而是那样太容易把工具当成思考的替代品。

那次进入 Instruments 之前,脑子里其实已经有几个怀疑方向了:

怀疑 1:图片链路把不该放在滚动期的工作放到了滚动期

这是信息流场景里最常见的嫌疑人。

尤其是:

  • 封面图尺寸不统一
  • 图文和视频卡片混排
  • 图片回填节奏不稳定
  • 显示前还做了圆角、阴影、缩放之类处理

如果这些工作在滚动过程中才真正发生,帧率很难稳。

怀疑 2:cell 的展示准备太晚了

很多列表一开始看起来“逻辑很清楚”,但真正上设备滚起来,就会暴露一个老问题:

大量展示准备工作放在了 cell 即将显示时才做。

例如:

  • 富文本拼装
  • 文案裁剪
  • 时间格式化
  • 高度计算
  • UI 状态判断
  • 埋点对象准备

单看每项都不算大,但一旦全都挤进滚动关键路径,就会从“还能接受”变成“整个列表发沉”。

怀疑 3:状态刷新范围太大

这种情况在响应式架构里特别常见。

表面上只是一条卡片状态变了,实际上触发的是:

  • section 级别重绑
  • 局部 diff 过多
  • 曝光上报和 UI 刷新耦合
  • 预加载回调频繁回主线程

这类问题最烦,因为它不一定会在某个函数上炸出特别高的峰值,但整体体验会一直差。

真正把方向收窄的,不是热点截图,而是几轮很便宜的小实验

那次真正把问题收窄,不是靠某一张漂亮的火焰图,而是靠几轮非常朴素的小实验。

实验一:把图片先全部替成占位图

这个实验非常粗暴,但特别有用。

一旦替掉图片,滚动明显回来了。
这一步并不能直接说明根因就是“图片太多”,但足够说明图像链路肯定是主方向之一。

这时候再继续问,就不再是泛泛的“是不是图片问题”,而是更具体的:

  • 解码是不是落在了主线程
  • 尺寸策略是不是不统一
  • 图片回填是不是导致重布局
  • 显示前是不是做了太多额外处理

实验二:把 cell 的一部分动态文本预处理掉

这一改,滚动也有明显改善。

这说明另一个方向也成立:
列表展示前的准备工作确实太晚了,而且这些工作不止一两项。

也就是说,问题不是单点,而是图像链路和展示链路一起重。

实验三:临时关掉曝光、预加载、自动播放这些边缘动作

这一轮实验之后,帧率继续变稳。

这时候问题基本已经很清楚了:
不是某一个致命瓶颈,而是首页信息流在滚动阶段承载了太多“顺手也做一下”的工作。

这些工作平时单看都合理:

  • 图片要回填
  • 曝光要上报
  • 预加载要做
  • 视频卡片要预热

但它们一起发生在滚动关键路径上,就会把整个列表拖垮。

真正的根因最后长什么样?不是一个点,而是一组坏决定

那次最后落地时,真正确认的问题大概是这样一组组合拳:

  • 图片尺寸策略不统一,导致显示前仍有处理成本
  • 部分图片解码时机太晚
  • cell 展示准备工作放在主线程现场做
  • 曝光和预加载回调在滚动期过于密集
  • 列表局部状态变化触发了比预期更大的刷新范围

这就是很多性能问题真实的样子。

不是“找到了第 357 行代码”就结束,
而是你最后会发现:这是几层设计都不够克制,最后在滚动阶段一起结算。

这也是为什么我越来越不喜欢那种“性能问题一定有一个关键函数”的讲法。
真实项目里当然也有这种情况,但更多时候不是“一个坏点”,而是“一组长期堆出来的坏形状”。

验证阶段最怕主观感受,必须回到同一条路径上比

改完之后,那次没有直接关问题,而是重新走了最开始那条复现路径。

同一台设备、同一个账号、同样的切换方式、同样滑到那段内容区,再看:

  • 掉帧是否还稳定出现
  • 主线程是不是还在同一段时间持续偏重
  • 图片回填和边缘行为是否还在和滚动抢主线程

这一步还顺手看了副作用:

  • 图片链路顺了之后,内存有没有明显抬高
  • 预处理做多了,首屏有没有变慢
  • 曝光上报延后,统计有没有被影响

性能优化里经常会犯一个错:
只盯“一个指标是不是好看了”,却不看是不是把别的问题换出来了。

但真实工作里,优化从来不是追单指标,而是在整体体验里做交换。
能接受的交换才叫优化,换出别的问题不叫。

这类文章最容易写成空话的地方,恰恰也是性能排查最真实的地方

很多总结文最后都会写成:

  • 要先定义问题
  • 要建立假设
  • 要验证结果

这些话当然没错,但它们太容易变成正确的废话。

真正有经验的差别,不在于会不会说这些话,而在于你有没有经历过下面这些时刻:

  • 一开始所有人说的都像一个问题,其实根本不是同一个问题
  • 第一眼最像根因的方向,最后只是伪线索
  • 工具里每个指标都像有异常,但真正主线只有一条
  • 最后的答案不是某个大峰值,而是一组小开销长期叠加

这些东西一旦做过几次,就会自然明白一件事:

性能排查真正值钱的,不是“我会用哪些工具”,而是“我能不能把一个混乱现场收拢成一条能动手的链路”。

这一点做不到,工具越多越乱。
这一点做到了,很多问题不用把所有图表都翻一遍,也能越来越接近根因。

我现在看性能问题,最在意的已经不是“能不能一眼找到答案”,而是能不能尽快让团队对下面这件事达成一致:

我们现在查的是同一个问题,而且已经知道它主要落在哪条链路上。

只要这件事成立,后面的修复通常都不会太飘。
真正难的,一直都不是最后那几行优化代码,而是前面把问题收拢到可以下手的形状。