第1章:渲染基础:Android渲染管线全流程解析(CPU/GPU协作机制)

各位同学,咱们今天聊聊Android渲染管线的全貌。说实话,我见过太多开发者一上来就扎进SurfaceFlinger或者GPU调优里,结果连最基本的CPU和GPU怎么配合都没搞明白。嗯,这就像你还没学会走路就想跑马拉松——不摔跟头才怪。

我个人习惯把渲染管线比作一条「流水线」。CPU负责准备原材料,GPU负责加工成品。两者之间有个缓冲区,叫BufferQueue。你想想看,如果CPU准备得太慢,GPU就得空转;如果GPU加工得太慢,CPU就得排队。这就是性能问题的根源。

1.1 从应用层到显示层:一次绘制的完整旅程

咱们先走一遍完整的流程。假设你在屏幕上点了一个按钮,系统要重绘一帧。整个过程分四步:

  1. 应用层绘制:CPU执行onDraw(),生成DisplayList
  2. 渲染线程处理:RenderThread把DisplayList转成GPU指令
  3. GPU执行:GPU执行指令,渲染到离屏缓冲区
  4. 合成与显示: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 实战:如何定位渲染瓶颈

说了这么多理论,咱们来点实际的。怎么定位渲染瓶颈?我一般用三步法:

  1. 看帧率:用Choreographer或者Profile GPU Rendering工具,看帧率是否稳定
  2. 看CPU/GPU时间:用Systrace或者Perfetto,看CPU和GPU各自花了多少时间
  3. 看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抖动。嗯,到时候见。