1. 渲染流水线概述:从App绘制到屏幕显示的完整链路
做过Android性能优化的同学都知道,卡顿是用户最容易感知的痛点。但你知道吗?很多时候我们优化了布局、减少了绘制,可卡顿依旧——因为问题出在整个渲染链条的另一环。
为什么需要理解这条链路?说白了,你看到的每一帧,都经历了「App 绘制 → 合成 → 屏幕刷新」三次握手。
核心认知:渲染性能不是 App 单方面的事。SurfaceFlinger 的合成策略、硬件的 VSync 信号、Buffer 队列的同步机制,每一个环节都可能成为瓶颈。
我个人习惯把这条链路比作「接力赛」:App 是第 1 棒,SurfaceFlinger 是第 2 棒,显示硬件是第 3 棒。任何一棒掉速,整帧就会迟到。
1.1 整条链路长什么样?
先看一个整体的框架图,我建议你把它刻在脑子里——因为后续所有性能分析都在拆解这张图里的某个节点。
1.2 每一步在做什么?
我们来拆开看看。嗯,这里要注意:每一节都有坑。
1.2.1 App 绘制阶段
当 VSync 信号到来,App 的 UI 线程开始处理输入事件、执行 measure/layout/draw。这些工作在 CPU 上进行,最终生成 DisplayList。然后 GPU 执行渲染指令,把结果写入 BufferQueue 的一个 Buffer 中。
我曾经在 一个新闻客户端里遇到掉帧问题:页面布局层层嵌套,光 measure 就花了 8ms,留给 GPU 渲染的时间只剩 3ms。这种 CPU 端的耗时会直接吃掉整帧的预算——优化布局嵌套比优化 GPU 更紧迫。
| 子阶段 | 耗时占比(典型) | 优化方向 |
|---|---|---|
| CPU 绘制 | 40%~60% | 减少布局层级、避免重复绘制 |
| GPU 渲染 | 20%~30% | 减少过度绘制、使用硬件加速 |
| Buffer 入队 | 5%~10% | 减少 Buffer 数量、合理使用 Triple Buffering |
1.2.2 BufferQueue 与同步
App 绘制完的帧不会直接给 SurfaceFlinger,而是放进一个共享队列。队列长度默认是 2(Double Buffering)或 3(Triple Buffering)。如果队列空了,SurfaceFlinger 会等待;如果队列满了,App 会阻塞——这就是 jank 的常见原因之一。
你想想看,当 App 绘制速度超过屏幕刷新率时,BufferQueue 会积累帧,导致「掉帧+卡顿」同时出现。我建议用 dumpsys SurfaceFlinger 查看 Buffer 的 pending 数量——超过 1 就必须警惕。
1.2.3 SurfaceFlinger 合成
SurfaceFlinger 负责把多个 App 的 Layer(图层)合并成一个显示帧。合成方式有两种:
- GPU 合成(OpenGL ES):通过 GPU 绘制多个图层到 FBO,灵活性高但耗电。
- HWC 合成:把图层交给显示硬件直接混合,功耗低、性能好。
HWC 不是万能的——它最多支持 4 层硬件覆盖层,超出部分会退回到 GPU 合成。我见过一个桌面小部件占用了一层硬件,然后其他图层全部走 GPU——卡到不行。
1.2.4 显示设备刷新
合成完成后,帧数据进入显示设备的 FrameBuffer。屏幕按照固定频率(通常是 60Hz,16.6ms 一帧)从 FrameBuffer 读取数据并显示。如果 SurfaceFlinger 没能在 VSync 前提交新帧,屏幕就复用上一帧——这就是 visual jank。
1.3 为什么理解这条链路很重要?
原因很直接:
- 定位瓶颈:卡顿来自 App 端、合成端还是显示端?没有链路认知,你只能盲目优化。
- 选择工具:Profile GPU Rendering 看 App 绘制,dumpsys gfxinfo 看 Buffer 状态,Systrace 看全局。你知道每个工具对应链路的哪个环节吗?
- 避免白费力气:我曾经花了 3 天优化一个动画的 onDraw 逻辑,结果发现瓶颈在 SurfaceFlinger 的 GPU 合成上——改动画效果完全没用。
一句话总结:渲染性能不是孤立的 App 问题,而是一条从 CPU、GPU 到显示硬件的流水线。理解整条链路,你才能知道该从哪里下手。
下一个知识点我会深入 SurfaceFlinger 的合成策略和 Buffer 管理——这些是高频瓶颈区,你最好提前熟悉一下 adb shell dumpsys SurfaceFlinger 的输出格式。