3. Android显示架构入门

帧率,说白了就是屏幕每秒刷新了多少张画面。30fps、60fps,大家都会看。但真正让我头疼的,从来不是平均帧率,而是那些“突然卡一下”的瞬间。

我记得刚接手座舱项目时,测试报告上写着“平均帧率58fps”,看起来挺漂亮。结果实车一跑,用户滑个地图都感觉像在放幻灯片。为什么?因为平均帧率把卡顿给“平均”掉了。所以,我们得把目光从帧率转向帧时间。

3.1 帧率波动:谁在捣乱?

帧率为什么会波动?我把它归结为三大类原因:

  • 渲染管线阻塞:GPU在等CPU,或者CPU在等GPU。说白了,就是干活的人没配合好。
  • 资源争抢:座舱里不是只有你在跑。导航、音乐、语音助手,哪个不要CPU?我遇到过最离谱的一次,是空调动画和地图渲染抢了同一个GPU通道。
  • 系统调度抖动:Linux内核在调度时,突然来了个高优先级中断,把你的渲染线程挤到一边去了。嗯,这种问题最难查。

核心观点:帧率波动不是随机的,它背后一定有明确的“肇事者”。我们的任务就是把它揪出来。

3.2 帧时间分解:把一帧拆开看

我个人习惯,拿到一个卡顿问题,第一件事就是做帧时间分解。把一帧从开始到显示,拆成几个阶段:

阶段 说明 常见瓶颈
App 处理 应用层逻辑计算、数据准备 主线程耗时操作、锁竞争
渲染提交 CPU 侧绘制指令提交到 GPU 过度绘制、无效重绘
GPU 渲染 GPU 执行绘制指令 着色器复杂、纹理过大
合成与送显 SurfaceFlinger/HWC 合成并输出 图层过多、带宽不足
VSync 等待 等待下一个垂直同步信号 帧时间超限、双缓冲阻塞

你想想看,如果一帧总耗时33ms,但App处理就占了25ms,那后面再怎么优化GPU也没用。我曾经在一个项目中,发现地图滑动卡顿,拆开一看,App处理阶段有个JSON解析占了18ms。换成二进制协议后,直接降到2ms。

我的习惯:用Systrace或Perfetto抓一段trace,把每一帧的各个阶段耗时标出来。哪个阶段柱子最高,就先打哪个。

3.3 Jank与Stutter:卡顿的两种面孔

这两个词经常混用,但在座舱性能优化里,我习惯把它们区分开:

  • Jank(跳帧):某一帧的帧时间突然暴增,导致画面“跳”了一下。比如正常16ms,突然来了个50ms的帧。
  • Stutter(微卡):连续几帧的帧时间都不稳定,画面感觉“抖”或者“不跟手”。

怎么识别?我一般看两个指标:

  1. 帧时间分布图:把每一帧的耗时画成散点图。Jank表现为孤立的“尖刺”,Stutter表现为一片“毛刺”。
  2. 卡顿率:统计帧时间超过阈值的帧数占比。比如60fps下,超过16.67ms就算一帧卡顿。

避坑指南:我曾经只看平均帧时间,结果被坑惨了。平均16ms,但实际有一半的帧是20ms,另一半是12ms。平均看起来没问题,但用户体感就是“一顿一顿的”。所以,一定要看分布,不要只看平均。

3.4 帧时间分析框架图

下面这张图,是我在做帧时间分析时常用的框架。它把从“原始数据”到“定位根因”的路径画了出来。

帧时间分析框架 数据采集 帧时间分解 指标计算 Systrace / Perfetto GPU 渲染时间 App 处理 / 渲染提交 合成 / VSync 等待 帧时间分布 / 卡顿率 Jank / Stutter 识别 定位瓶颈阶段 CPU / GPU / 合成 分析根因 锁竞争 / 过度绘制 / 调度 输出优化方案 代码 / 配置 / 资源 验证闭环:复测 → 对比 → 确认优化效果

3.5 实战:如何快速识别Jank和Stutter

我分享一个我自己常用的方法,不依赖复杂工具,用dumpsys就能做初步判断:

# 抓取 SurfaceFlinger 的帧时间信息
adb shell dumpsys SurfaceFlinger --latency

# 输出格式:每一行代表一帧
# 列含义:时间戳 | 帧开始时间 | 帧结束时间
# 帧时间 = 帧结束时间 - 帧开始时间

拿到数据后,我一般写个简单的Python脚本,把帧时间画成折线图。一眼就能看出哪里是Jank,哪里是Stutter。

判断标准(我自己的经验值):

  • 单帧帧时间 > 50ms:严重Jank,必须查。
  • 连续3帧帧时间 > 20ms:Stutter,需要优化。
  • 帧时间标准差 > 5ms:系统稳定性有问题。

嗯,这里要注意一点:不要只看最大值。我见过有人把最大值从100ms优化到30ms,就以为搞定了。结果一看分布,大部分帧还是20ms以上。用户体感依然很差。所以,我建议关注P95(95分位值)和P99,这两个指标比平均值更能反映真实体验。

一个小技巧:在座舱里,我习惯把帧时间目标定得比标准更严一点。比如60fps的目标,我要求P99帧时间不超过20ms。留出余量,才能应对突发负载。