iOS 性能优化 系列 07|一次真实的 iOS 性能排查,是怎样收敛到根因的
真正难的不是打开 Instruments,而是先判断自己看到的到底是不是同一个问题,再把一堆伪线索一层层排掉
做 iOS 性能排查,最烦的不是工具难用,而是现场经常一开始就是乱的。
产品说首页卡,测试说是列表掉帧,开发觉得可能是图片,后端又怀疑接口慢。
每个人说的都像是一个问题,但真开始看时,往往会发现大家描述的根本不是同一件事。
这类活我做过几次之后,最大的感受不是“哪个工具最重要”,而是:
性能排查首先是收敛问题,其次才是分析数据。
如果问题没有先收敛,工具开得越多,越容易把自己带偏。
因为你看到的每个指标都像有信息,但每个信息都不够构成结论。
下面这篇文章,不讲“理论上怎么排查”,而是按一次真实项目里会发生的节奏往下写:
一个首页信息流掉帧的问题,是怎么一步一步从“感觉有点卡”,收敛到可以动手改的。
先把场景钉死,不然后面所有分析都会飘
那次问题最开始报上来的描述很普通:
- 首页滑动不顺
- 某些机型更明显
- 最近版本感觉更卡了
这种描述听起来像有信息,其实几乎不能直接拿来排查。
因为它至少混了三层东西:
- 页面范围不清楚
- 现象类型不清楚
- 复现条件不清楚
真正能开始工作的描述,最后被压成了这样:
iPhone 13 Pro,iOS 18,登录态账号 A,冷启动进入首页推荐流,首屏加载完成后切到“关注”tab,再切回推荐流,连续快速上滑 4 屏,从第 3 屏开始持续掉帧,问题主要出现在图文混排和视频卡片混排区。
这段话的重要性,不在于它写得像测试用例,而在于它一下子把很多歧义砍掉了:
- 不是全 App 卡,是首页推荐流卡
- 不是首次进入卡,是第二次进入更明显
- 不是点击慢,是滚动掉帧
- 不是所有区域都一样,是图文混排区更明显
排查真正开始,往往就是从这种“把范围钉死”开始的。
没有这一步,后面所有“分析”都可能只是在追自己的想象。
先别急着看工具,先判断这到底是偶发卡顿还是持续低帧
“滑动不顺”这四个字,实际可能对应完全不同的问题。
那次我最先做的,不是开 Instruments,而是先录屏、反复滑几次,把现象定性。
最后定下来,这不是那种“偶尔抖一下”的卡,而是很典型的持续低帧:
- 一旦进入特定内容区,连续几屏都不顺
- 不是某一帧突然爆炸,而是整个滚动阶段都发沉
- 用户主观感受不是“突然卡了一下”,而是“这段一直滑不动”
这个区分非常重要。
因为偶发卡顿通常更像:
- 某次图片解码突然砸在主线程
- 某个大对象初始化时机不对
- 某个 layout pass 偶尔变重
而持续低帧更像:
- 每帧都在做重复工作
- cell 结构本身太重
- 滚动期间不断有异步结果回填
- 列表状态刷新范围过大
如果一开始没把这两类问题分开,后面看任何数据都容易误读。
复现路径一定要写下来,不然“改好了”这种话没有任何意义
性能问题最容易出现的假象之一,就是“刚才还挺明显,现在怎么又没了”。
所以那次排查里,我先做的一个动作很笨,但非常值钱:把复现路径写成固定步骤。
当时整理出来的是:
- 杀掉 App,重新冷启动
- 登录账号 A
- 进入首页推荐流,等待首屏完全稳定
- 切换到“关注”tab
- 再切回推荐流
- 快速连续上滑 4 屏
- 观察第 3 屏开始的帧率表现和主线程占用
同时把环境也记死:
- 设备型号
- 系统版本
- 网络条件
- 数据量级
- 是否打开调试开关
这件事的意义不只是方便别人复现,更重要的是方便后面验证。
因为性能优化里最不值钱的一句话就是:
我感觉比刚才顺了。
没有固定路径,“顺了”这两个字没有分析价值。
它可能只是因为数据不一样、网络不一样、页面没滑到同一段,甚至只是这次手指没滑那么快。
第一轮判断,不找根因,只判断问题落在哪条链路上
很多人一开始就想问:
- 到底是哪一行代码慢
- 到底哪个函数最耗时
- 到底是哪个库的问题
但真实排查里,这样问通常太早。
那次我先做的,不是抓“最终元凶”,而是粗分链路:
是数据问题,还是渲染问题
这一步很关键,因为首页慢很容易让人本能怀疑接口。
我先做了一个非常直接的验证:
尽量固定网络结果,让第二次进入推荐流时数据尽量来自已有结果,而不是网络变动。
结果很明确:
数据已经回来了,滚动照样沉,而且是稳定沉。
这一步基本就能把“接口慢导致滚动卡”从主问题里排掉。
接口慢会影响首屏时间,但它不太解释“第二次进入后、某段内容开始持续低帧”。
是主线程重,还是后台工作不断打回主线程
接下来看的就不是“卡不卡”,而是卡的时候主线程在干什么。
这一步的目标也不是一上来就找到哪一帧,而是先判断问题形态:
- 是主线程持续偏忙
- 还是后台回调太密,主线程被一波一波打断
最后看到的情况更偏前者:
主线程并不是偶尔炸一下,而是在滚动阶段一直偏重。
这就说明方向开始变清楚了:
- 重点不在网络
- 重点不在单次大任务
- 重点更像列表展示链路本身太重
是单点异常,还是系统性过载
这是我每次都很看重的一个判断。
如果只有一个函数偶发跑出 200ms,那很好办,抓它就行。
但那次不是这种问题。
那次真正麻烦的地方在于,没有哪个点离谱到“一眼真凶”,而是很多本来不算夸张的小开销叠在一起,全部落到了滚动关键路径上。
这种问题最容易让人烦躁,因为它不像 crash 有一条特别清晰的栈。
它更像系统在告诉你:你过去每一个“先这么写着”的小决定,现在开始一起收费了。
到这一步才值得开 Instruments,而且一定是带着怀疑去看
我一直不太喜欢那种“先全开工具再说”的排查方式。
不是工具没用,而是那样太容易把工具当成思考的替代品。
那次进入 Instruments 之前,脑子里其实已经有几个怀疑方向了:
怀疑 1:图片链路把不该放在滚动期的工作放到了滚动期
这是信息流场景里最常见的嫌疑人。
尤其是:
- 封面图尺寸不统一
- 图文和视频卡片混排
- 图片回填节奏不稳定
- 显示前还做了圆角、阴影、缩放之类处理
如果这些工作在滚动过程中才真正发生,帧率很难稳。
怀疑 2:cell 的展示准备太晚了
很多列表一开始看起来“逻辑很清楚”,但真正上设备滚起来,就会暴露一个老问题:
大量展示准备工作放在了 cell 即将显示时才做。
例如:
- 富文本拼装
- 文案裁剪
- 时间格式化
- 高度计算
- UI 状态判断
- 埋点对象准备
单看每项都不算大,但一旦全都挤进滚动关键路径,就会从“还能接受”变成“整个列表发沉”。
怀疑 3:状态刷新范围太大
这种情况在响应式架构里特别常见。
表面上只是一条卡片状态变了,实际上触发的是:
- section 级别重绑
- 局部 diff 过多
- 曝光上报和 UI 刷新耦合
- 预加载回调频繁回主线程
这类问题最烦,因为它不一定会在某个函数上炸出特别高的峰值,但整体体验会一直差。
真正把方向收窄的,不是热点截图,而是几轮很便宜的小实验
那次真正把问题收窄,不是靠某一张漂亮的火焰图,而是靠几轮非常朴素的小实验。
实验一:把图片先全部替成占位图
这个实验非常粗暴,但特别有用。
一旦替掉图片,滚动明显回来了。
这一步并不能直接说明根因就是“图片太多”,但足够说明图像链路肯定是主方向之一。
这时候再继续问,就不再是泛泛的“是不是图片问题”,而是更具体的:
- 解码是不是落在了主线程
- 尺寸策略是不是不统一
- 图片回填是不是导致重布局
- 显示前是不是做了太多额外处理
实验二:把 cell 的一部分动态文本预处理掉
这一改,滚动也有明显改善。
这说明另一个方向也成立:
列表展示前的准备工作确实太晚了,而且这些工作不止一两项。
也就是说,问题不是单点,而是图像链路和展示链路一起重。
实验三:临时关掉曝光、预加载、自动播放这些边缘动作
这一轮实验之后,帧率继续变稳。
这时候问题基本已经很清楚了:
不是某一个致命瓶颈,而是首页信息流在滚动阶段承载了太多“顺手也做一下”的工作。
这些工作平时单看都合理:
- 图片要回填
- 曝光要上报
- 预加载要做
- 视频卡片要预热
但它们一起发生在滚动关键路径上,就会把整个列表拖垮。
真正的根因最后长什么样?不是一个点,而是一组坏决定
那次最后落地时,真正确认的问题大概是这样一组组合拳:
- 图片尺寸策略不统一,导致显示前仍有处理成本
- 部分图片解码时机太晚
- cell 展示准备工作放在主线程现场做
- 曝光和预加载回调在滚动期过于密集
- 列表局部状态变化触发了比预期更大的刷新范围
这就是很多性能问题真实的样子。
不是“找到了第 357 行代码”就结束,
而是你最后会发现:这是几层设计都不够克制,最后在滚动阶段一起结算。
这也是为什么我越来越不喜欢那种“性能问题一定有一个关键函数”的讲法。
真实项目里当然也有这种情况,但更多时候不是“一个坏点”,而是“一组长期堆出来的坏形状”。
验证阶段最怕主观感受,必须回到同一条路径上比
改完之后,那次没有直接关问题,而是重新走了最开始那条复现路径。
同一台设备、同一个账号、同样的切换方式、同样滑到那段内容区,再看:
- 掉帧是否还稳定出现
- 主线程是不是还在同一段时间持续偏重
- 图片回填和边缘行为是否还在和滚动抢主线程
这一步还顺手看了副作用:
- 图片链路顺了之后,内存有没有明显抬高
- 预处理做多了,首屏有没有变慢
- 曝光上报延后,统计有没有被影响
性能优化里经常会犯一个错:
只盯“一个指标是不是好看了”,却不看是不是把别的问题换出来了。
但真实工作里,优化从来不是追单指标,而是在整体体验里做交换。
能接受的交换才叫优化,换出别的问题不叫。
这类文章最容易写成空话的地方,恰恰也是性能排查最真实的地方
很多总结文最后都会写成:
- 要先定义问题
- 要建立假设
- 要验证结果
这些话当然没错,但它们太容易变成正确的废话。
真正有经验的差别,不在于会不会说这些话,而在于你有没有经历过下面这些时刻:
- 一开始所有人说的都像一个问题,其实根本不是同一个问题
- 第一眼最像根因的方向,最后只是伪线索
- 工具里每个指标都像有异常,但真正主线只有一条
- 最后的答案不是某个大峰值,而是一组小开销长期叠加
这些东西一旦做过几次,就会自然明白一件事:
性能排查真正值钱的,不是“我会用哪些工具”,而是“我能不能把一个混乱现场收拢成一条能动手的链路”。
这一点做不到,工具越多越乱。
这一点做到了,很多问题不用把所有图表都翻一遍,也能越来越接近根因。
我现在看性能问题,最在意的已经不是“能不能一眼找到答案”,而是能不能尽快让团队对下面这件事达成一致:
我们现在查的是同一个问题,而且已经知道它主要落在哪条链路上。
只要这件事成立,后面的修复通常都不会太飘。
真正难的,一直都不是最后那几行优化代码,而是前面把问题收拢到可以下手的形状。