CPU与GPU协同:渲染任务分工,瓶颈场景详解
咱们做Android渲染优化的,说白了就是在跟两个家伙打交道:CPU和GPU。很多同学一上来就盯着帧率看,觉得掉帧就是GPU不行。其实啊,我得说,掉帧的原因至少有一半出在CPU身上。今天我就把这俩的活儿掰开讲讲,再结合我踩过的坑,帮你快速定位瓶颈。
一、渲染流水线里谁干什么活
先画个总图,你心里有个谱。这张图我记得很清楚,每次培训新人我都先画它。
核心认知:CPU负责「想清楚画什么」,GPU负责「执行画」。但CPU如果「想」得太慢,GPU就会饿死;GPU如果「画」得太慢,CPU就得等着下一帧。
二、CPU瓶颈的典型场景
我个人习惯先用systrace抓一把,看CPU阶段的时间戳。如果Choreographer.doFrame或者View.draw那一块突然变长,那八九不离十是CPU瓶颈。下面列几个我实际项目里碰到最多的。
1. 布局嵌套过深
你想想看,一个页面五六层RelativeLayout套LinearLayout,每次measure都要从根遍历到底。我在给某个电商App做优化时,发现首页商品卡片打开要200ms。一查,一个商品条目嵌套了11层。去掉不必要的容器,直接降到18ms。
提示:用ConstraintLayout或者FlatLayout尽量压平层次。我建议每个页面布局深度控制在4层以内。
2. 过度绘制(Overdraw)导致CPU生成指令过多
其实Overdraw不只是GPU的活——CPU生成绘制指令时,如果同一个像素被重复画,CPU的指令队列也会膨胀。我曾经见过一个同事写了个背景色在Activity的根布局上,然后每个Fragment又设了一层背景,每条Item还设了背景……三层叠加。用开发者选项 → 调试GPU过度绘制一看,满屏紫红色。去掉重复背景后,CPU绘制指令数少了60%。
3. 耗时方法卡住UI线程
这个是最常见的。比如在onClick里直接做网络请求、数据库读写,或者解析JSON。UI线程一旦被卡住,Choreographer过了vsync点才收到回调,下一帧就直接掉帧了。我记得有次线上反馈滑动列表卡顿,查了三天发现是某次版本里在Adapter的getView里写了个BitmapFactory.decodeFile……嗯,从那以后我们团队强制所有文件操作放到异步线程。
4. 垃圾回收(GC)抖动
这个特别隐蔽。如果你的代码频繁创建临时对象(比如在onDraw里new Paint),GC就会频繁触发,暂停UI线程。我在优化一个视频播放器时,发现每次播放下一个视频前会掉一两帧。用systrace一看,GC_FOR_ALLOC经常出现。我们让播放器预分配对象池,帧率立马就稳了。
| CPU瓶颈类型 | 典型现象 | 排查手段 |
|---|---|---|
| 布局嵌套过深 | measure/layout耗时>5ms | systrace + Layout Inspector |
| 过度绘制 | GPU渲染指令过多 | 开发者选项 → Overdraw调试 |
| UI线程阻塞 | doFrame长时间无返回 | systrace + 代码review |
| GC频繁 | 每帧出现GC_FOR_ALLOC | Memory Profiler + 对象分配追踪 |
三、GPU瓶颈的典型场景
好,接下来看GPU端。如果CPU阶段很顺畅,但是帧率依然不稳,那问题大概率出在GPU。
1. 纹理过大或过多
GPU处理纹理的能力是有限的。特别是OpenGL ES的纹理尺寸有上限,一般是4096x4096。我见过有人给列表Item的图片用了2000x2000的原图,GPU栅格化直接炸了。用Profiling工具看,GPU帧时间飙升到60ms。解决办法很简单——压缩到匹配显示尺寸,或者用Bitmap.Config.RGB_565减少内存带宽。
警告:别用RGB_565处理有渐变的图片,色带会很难看。拍照场景建议用ARGB_8888,但尺寸一定要缩放。
2. 复杂Shader或混合效果
如果你在自定义View里用了setLayerType(LAYER_TYPE_HARDWARE)然后又叠加多个BlendMode,GPU的片元着色器压力会很大。有一次我给一个时钟App画表盘,用了圆形渐变+阴影+抗锯齿,结果在低端机上连30帧都跑不到。后来用预渲染bitmap代替实时绘制,直接解决了。
3. SurfaceFlinger合成瓶颈
这个比较进阶。当你的App有多个Window、Surface、Layer时,SurfaceFlinger会在GPU里做layer合成。如果Layer数超过硬件合成器(HWC)支持的overlay数量,就会退回到GPU合成,性能暴跌。我记得在Android 8.0的一个项目里,我们通过合并Surface(比如把悬浮窗合并到Activity的Surface上)把Layer数从7降到4,帧时间降了一半。
4. 帧率与刷新率不匹配(掉帧渲染)
GPU渲染完一帧后,如果来不及在下一个vsync前完成,就会重复上一帧。这本质是GPU处理能力不足。我建议用dumpsys gfxinfo看Draw和Process时间。如果Draw时间超过16ms(60Hz下),那就真得降特效了。
快速判定法:systrace里CPU阶段正常(<10ms),但是GPU提交后等了很久才显示,那基本是GPU瓶颈。反过来,CPU阶段长,GPU空闲,就是CPU瓶颈。
四、避坑指南——我踩过的几个坑
说到底,CPU和GPU是搭档,哪个拖后腿都不行。我总结几个小建议:
- 别迷信硬件加速。硬件加速不是万能药,复杂Path在GPU上反而慢,有时候软件绘制更快。我在做手写笔记App时,发现笔迹渲染用软件绘制比硬件快2倍,因为GPU处理大量小路径的开销太高。
- 关注BufferQueue的dequeueBuffer超时。如果GPU太忙,CPU在dequeue时可能会等很久。systrace里如果看到
dequeueBuffer飘红,就是GPU被前几帧堵住了。 - VSync对齐非常重要。我曾经把一个动画的帧率从60降到30,理论上应该省电。但因为没对齐vsync,反而导致丢帧感更强。后来改成每两帧跳一帧,视觉上才平滑。
终极调试命令:adb shell dumpsys SurfaceFlinger --latency SurfaceView 可以直观看到每一帧的提交时间,对着数字分析,比凭感觉调强一百倍。
好了,CPU和GPU这哥俩的分工和典型瓶颈先说到这。记住一点:别只看帧率,要拆开看每个阶段耗时。用systrace,用dumpsys,用Perfetto,把时间打在屏幕上,哪儿疼治哪儿。