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(微卡):连续几帧的帧时间都不稳定,画面感觉“抖”或者“不跟手”。
怎么识别?我一般看两个指标:
- 帧时间分布图:把每一帧的耗时画成散点图。Jank表现为孤立的“尖刺”,Stutter表现为一片“毛刺”。
- 卡顿率:统计帧时间超过阈值的帧数占比。比如60fps下,超过16.67ms就算一帧卡顿。
避坑指南:我曾经只看平均帧时间,结果被坑惨了。平均16ms,但实际有一半的帧是20ms,另一半是12ms。平均看起来没问题,但用户体感就是“一顿一顿的”。所以,一定要看分布,不要只看平均。
3.4 帧时间分析框架图
下面这张图,是我在做帧时间分析时常用的框架。它把从“原始数据”到“定位根因”的路径画了出来。
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。留出余量,才能应对突发负载。