Android 渲染机制与 VSync 归纳总结
讲到 SystemUI 的启动帧率监控,绝绕不开 Android 的渲染机制和 VSync。我见过不少同学上来就抓 Trace,看到掉帧就一脸懵,说白了就是没搞明白底层这个「拧螺丝」的过程。今天咱们把这块掰开揉碎,你理解了这些,再去看 Trace 里的掉帧段就会一目了然。
渲染流水线:CPU → GPU → 屏幕
Android 每帧的渲染大致分三步走:
- CPU 阶段:处理输入事件、布局测量、绘制指令(View.draw 的调用),最后生成 DisplayList。
- GPU 阶段:接收 DisplayList,进行栅格化,输出到帧缓冲(Buffer)。
- 屏幕刷新:从 Buffer 取数据,按像素刷新显示。
这三步不是任意时刻都能同时跑的。硬件会固定发一个信号告诉系统:「老哥,该换下一帧了!」这个信号就是 VSync(垂直同步)。
VSync 的作用与信号模型
VSync 的典型频率是 60Hz(也有 90Hz、120Hz)。每次信号到来,系统就启动一帧的渲染。如果没有 VSync,CPU/GPU 可能一帧还没画完,屏幕就强行读取 Buffer,画面就撕裂了。
Android 的「Choreographer」就是靠 VSync 驱动的。它会等待下一个信号到达,然后回调 doFrame,通知应用层开始绘制。
双缓冲与三缓冲
为了不让 CPU/GPU 和屏幕抢同一块 Buffer,Android 用了「双缓冲」甚至「三缓冲」。看看这个对比:
| 缓冲方案 | 结构 | 优点 | 缺点 | 常见场景 |
|---|---|---|---|---|
| 双缓冲 | Front Buffer + Back Buffer | 简单,无撕裂 | 如果一帧超时,下一帧会丢 | 老设备、低端机 |
| 三缓冲 | Front + Back + Pending | 允许一帧延迟,不容易丢帧 | 增加内存占用,可能带来额外延迟 | 高帧率、复杂动画 |
三缓冲其实是在双缓冲基础上多了一个「等待队列」的 Buffer。当 GPU 来不及在 VSync 前完成当前帧,系统可以从第三个 Buffer 里拿一个已经准备好的旧帧直接给屏幕,避免屏幕空转。代价是反应稍微慢一拍。
你想想看,SystemUI 启动时会有大量的 View 创建和绘制,如果 CPU 调度不及时,单靠双缓冲很容易掉帧。很多厂商会默认开三缓冲,但我们做监控时要知道:掉帧不一定是代码慢,可能是缓冲切换机制造成的。
掉帧的本质:超时未完成
每帧必须在 VSync 间隔内完成 CPU + GPU 的工作。如果超了,就会出现「丢帧」或「延迟帧」。在 Systrace 里你看到的 jank 就是这个意思。
常见导致超时的原因:
- 主线程有耗时操作(比如反射、IO)
- 过度绘制太多,GPU 负载过重
- 垃圾回收(GC)阻塞了渲染线程
- Binder 调用阻塞
小结:监控中要关注什么?
搞明白上面这些,你就能站在工程角度判断:
- 如果 Systrace 显示 App 帧耗时集中在 CPU 阶段——多半是主线程太忙。
- 如果 GPU 阶段占满——可能是过度绘制或者着色器复杂。
- 如果 Buffer 切换出现空档(丢帧)——大概率是三缓冲策略没生效,或者内存交换慢。
我习惯在 SystemUI 启动时用 FrameMetricsAggregator 直接记录每帧的 DRAW、SYNC、SWAP_BUFFERS 等阶段耗时。再配合 VSync 时序,一眼就能看出哪一段「拖了全班后腿」。嗯,下节课我们就专门讲怎么用这些工具做监控。