第1章:渲染基础:Android渲染管线全流程解析(CPU/GPU协作机制)
各位同学,咱们今天聊聊Android渲染管线的全貌。说实话,我见过太多开发者一上来就扎进SurfaceFlinger或者GPU调优里,结果连最基本的CPU和GPU怎么配合都没搞明白。嗯,这就像你还没学会走路就想跑马拉松——不摔跟头才怪。
我个人习惯把渲染管线比作一条「流水线」。CPU负责准备原材料,GPU负责加工成品。两者之间有个缓冲区,叫BufferQueue。你想想看,如果CPU准备得太慢,GPU就得空转;如果GPU加工得太慢,CPU就得排队。这就是性能问题的根源。
1.1 从应用层到显示层:一次绘制的完整旅程
咱们先走一遍完整的流程。假设你在屏幕上点了一个按钮,系统要重绘一帧。整个过程分四步:
- 应用层绘制:CPU执行onDraw(),生成DisplayList
- 渲染线程处理:RenderThread把DisplayList转成GPU指令
- GPU执行:GPU执行指令,渲染到离屏缓冲区
- 合成与显示:SurfaceFlinger合成各层,送到屏幕
我在项目中遇到过一个问题:某个页面滑动特别卡,一查发现onDraw()里做了大量Bitmap解码。CPU忙得不可开交,GPU却在旁边干等着。说白了,这就是CPU瓶颈。
核心要点:每一帧的绘制时间不能超过16.67ms(60fps)。CPU和GPU各占一部分时间,任何一方超时都会导致掉帧。
1.2 CPU阶段:从View到DisplayList
CPU阶段主要做三件事:测量、布局、绘制。我刚开始做Android开发时,总觉得这三个步骤是串行的。后来才发现,从Android 5.0开始,系统引入了RenderThread,把绘制指令的生成和实际渲染分开了。
具体来说:
- 主线程:执行View的onMeasure()、onLayout()、onDraw(),生成DisplayList
- RenderThread:把DisplayList解析成OpenGL/Vulkan指令,提交给GPU
这里有个坑,我曾经踩过:如果你在onDraw()里频繁创建新的Paint对象或者Path对象,主线程就会被拖慢。因为每次创建对象都要分配内存,触发GC。GC一跑,你的帧率就崩了。
避坑指南:我曾经在项目里把所有Paint对象都缓存起来,在onDraw()里只复用不新建。结果帧率从45fps直接升到60fps。就这么简单的一个改动。
1.3 GPU阶段:从指令到像素
GPU拿到指令后,开始干活。它的工作流程大致是:
| 阶段 | 说明 | 性能关键点 |
|---|---|---|
| 顶点处理 | 计算每个顶点的位置和属性 | 顶点数量越少越好 |
| 光栅化 | 把顶点数据转成像素 | 分辨率越高,像素越多 |
| 片段处理 | 计算每个像素的颜色 | Shader复杂度影响大 |
| 输出合并 | 写入帧缓冲区 | Overdraw是杀手 |
你想想看,GPU最怕什么?最怕Overdraw。就是同一个像素被反复绘制多次。比如你有一个半透明的背景,上面叠了一个不透明的按钮,再叠一个半透明的弹窗。GPU得把这三个图层都算一遍,最后才合成。白白浪费算力。
注意:在开发者选项里打开「显示过度绘制区域」,红色区域越多,性能越差。我见过一个页面全屏都是深红色,一查发现是背景层和内容层重叠了三次。去掉一层背景后,帧率直接翻倍。
1.4 CPU与GPU的协作机制:同步与异步
CPU和GPU怎么配合?说白了就是「生产者-消费者」模式。CPU生产指令,GPU消费指令。中间有个缓冲区,叫BufferQueue。
具体流程:
- CPU把渲染好的帧放到BufferQueue里
- SurfaceFlinger从BufferQueue里取帧
- 如果BufferQueue满了,CPU就得等
- 如果BufferQueue空了,GPU就得等
我记得有一次排查卡顿问题,发现BufferQueue里总是只有一帧。这意味着CPU和GPU几乎同步工作,没有任何缓冲。一旦CPU稍微慢一点,GPU就得空转。解决方案很简单:把BufferQueue的容量从1改成2,让CPU可以提前准备下一帧。
关键指标:
- BufferQueue深度:建议2-3帧
- 帧率:稳定60fps
- 掉帧率:低于5%
- GPU利用率:60%-80%为佳
1.5 实战:如何定位渲染瓶颈
说了这么多理论,咱们来点实际的。怎么定位渲染瓶颈?我一般用三步法:
- 看帧率:用Choreographer或者Profile GPU Rendering工具,看帧率是否稳定
- 看CPU/GPU时间:用Systrace或者Perfetto,看CPU和GPU各自花了多少时间
- 看Overdraw:用开发者选项里的「显示过度绘制区域」,看有没有不必要的重绘
举个例子,我曾经优化过一个列表页面。用Systrace一看,CPU时间只有5ms,但GPU时间高达20ms。这说明瓶颈在GPU。再打开Overdraw检查,发现列表项的阴影效果导致每个像素被绘制了4次。去掉阴影后,GPU时间降到8ms,帧率从30fps升到60fps。
小技巧:我个人习惯在代码里加一些自定义的Trace标记,比如Trace.beginSection("onDraw")。这样在Systrace里就能精确看到每个方法的耗时。比瞎猜靠谱多了。
1.6 总结:记住这三句话
好了,这一章的内容就这些。最后我总结三句话,你记住就行:
- CPU负责准备,GPU负责渲染——谁慢谁就是瓶颈
- BufferQueue是缓冲区——深度不够就卡顿
- Overdraw是性能杀手——能少画就少画
下一章咱们聊聊「CPU性能优化:减少主线程负载」。到时候我会分享一些实际项目里的优化案例,包括怎么用异步布局、怎么避免GC抖动。嗯,到时候见。