10、GPU渲染优化:过度绘制检测与消除、ClipRect与硬件加速、Shader复杂度优化
说到GPU渲染优化,我脑子里第一个蹦出来的词就是「过度绘制」。这玩意儿,说白了就是同一个像素点被反复画了好几次。你想想看,屏幕上的每个像素最终只显示一种颜色,但你却在背后偷偷画了它三四遍,这不是白费力气吗?
我在项目中遇到过最夸张的一次,某个页面的过度绘制达到了5x。什么意思?就是同一个像素被画了5遍。那台低端机直接掉帧到20fps,用户反馈说「滑动像PPT」。嗯,从那以后,我就把过度绘制检测列为了性能优化的第一道关卡。
10.1 过度绘制检测与消除
检测过度绘制,Android自带的工具就够用。开发者选项里有个「调试GPU过度绘制」开关,打开后屏幕会变成五颜六色的。
- 蓝色:1x过度绘制,正常
- 绿色:2x过度绘制,可以接受
- 浅红:3x过度绘制,需要关注
- 深红:4x以上,必须优化
我个人习惯,看到深红色区域就直接标记为「性能黑洞」。怎么消除?核心思路就一条:减少不必要的背景绘制。
常见过度绘制场景及解法:
- 父布局和子布局都设置了背景色 → 只保留最上层的背景
- 多个Fragment叠加,每个都画了背景 → 只让最顶层的Fragment画背景
- ListView/RecyclerView的item背景和整个列表背景重复 → 去掉item背景
- Window背景 + Activity布局背景 + 根布局背景 → 只保留一个
我曾经接手过一个项目,整个应用都是深红色。排查后发现,每个Activity都调用了setContentView,而主题里又默认设置了Window背景。结果就是:Window画一次,DecorView画一次,根布局又画一次。三层背景叠加,直接3x过度绘制。解决方案很简单——在主题里把android:windowBackground设为@null,或者只在根布局设置背景。
// 在styles.xml中
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="android:windowBackground">@null</item>
</style>
就这么一行代码,整个应用的过度绘制从3x降到了1x。你说值不值?
10.2 ClipRect与硬件加速
ClipRect是个好东西,但很多人没用对。它的作用就是告诉GPU:「你只需要画这个矩形范围内的东西,外面的别管。」
为什么需要这个?因为GPU在绘制时,默认会处理所有传入的绘制指令。如果你有一个很大的View,但只有一小部分可见,GPU还是会傻乎乎地把整个View都画一遍。这时候用canvas.clipRect(),就能让GPU只处理可见区域。
使用ClipRect的典型场景:
- 列表滚动时,只绘制可见区域的item
- 自定义View中,只绘制需要更新的局部区域
- 动画过程中,限制绘制范围到变化区域
我记得有一次优化一个自定义的图表View,每次数据更新都要重绘整个图表,性能很差。后来我用clipRect只重绘数据变化的区域,帧率从30fps直接飙到55fps。你想想看,GPU省了多少力气。
说到硬件加速,这里有个坑。Android从3.0开始默认开启硬件加速,但很多人不知道它有个限制:硬件加速不支持所有的Canvas操作。比如clipPath()在硬件加速下就不支持,如果你用了,系统会默默走软件绘制,性能直接崩盘。
硬件加速的注意事项:
- 避免使用
clipPath(),改用clipRect()或clipOutRect() - 避免在
onDraw()中创建新的Paint对象 - 避免使用
drawBitmap()时频繁缩放 - 使用
View.setLayerType()时,谨慎选择LAYER_TYPE_HARDWARE
我曾经遇到过一个Bug:某个页面在低端机上滑动卡顿,但在高端机上完全正常。排查后发现,自定义View里用了clipPath来做圆角裁剪。高端机GPU强,勉强撑住了;低端机直接跪了。改成clipRect配合OutlineProvider后,问题解决。
10.3 Shader复杂度优化
Shader,说白了就是GPU上跑的小程序。Fragment Shader负责计算每个像素的颜色,如果它太复杂,GPU就得花更多时间处理每个像素。
我见过最离谱的Shader,里面有一个for循环,循环次数还是动态的。你想想看,一个1080p的屏幕有200多万个像素,每个像素都要跑一遍这个循环,GPU不炸才怪。
Shader优化的核心原则:
- 减少条件分支:GPU是并行处理器,分支会导致所有线程都要走两条路径
- 避免循环:尤其是动态循环,能展开就展开
- 减少纹理采样次数:每次采样都有开销,能合并就合并
- 使用低精度计算:能用
mediump就别用highp
// 不推荐的Shader写法
void main() {
vec4 color = vec4(0.0);
for (int i = 0; i < 10; i++) {
if (i % 2 == 0) {
color += texture2D(uTexture, vTexCoord + vec2(i * 0.01, 0.0));
} else {
color += texture2D(uTexture, vTexCoord - vec2(i * 0.01, 0.0));
}
}
gl_FragColor = color / 10.0;
}
// 优化后的写法
void main() {
vec4 color = texture2D(uTexture, vTexCoord);
color += texture2D(uTexture, vTexCoord + vec2(0.01, 0.0));
color += texture2D(uTexture, vTexCoord - vec2(0.01, 0.0));
color += texture2D(uTexture, vTexCoord + vec2(0.02, 0.0));
color += texture2D(uTexture, vTexCoord - vec2(0.02, 0.0));
gl_FragColor = color / 5.0;
}
看到了吗?优化后的版本去掉了循环和分支,直接展开。虽然代码长了点,但GPU跑起来快得多。
Shader优化的几个关键指标:
| 指标 | 说明 | 优化目标 |
|---|---|---|
| 指令数 | Shader中指令的总数 | 越少越好,控制在100条以内 |
| 纹理采样次数 | 每个像素采样的纹理数量 | 不超过4次 |
| 分支复杂度 | 条件分支的嵌套深度 | 避免嵌套,最好无分支 |
| 精度 | 使用的浮点精度等级 | 能用mediump不用highp |
我个人习惯,写完Shader后会用GLSL Validator检查一下,看看有没有性能隐患。另外,不要在Fragment Shader里做复杂的数学运算,比如pow、sin、cos这些,能提前算好的就提前算好,传到Shader里直接用。
嗯,说到这我想起一个案例。之前做一个实时滤镜功能,Fragment Shader里用了pow函数来做颜色校正。在高端机上没问题,但低端机上帧率直接掉了一半。后来我把pow的结果提前算好,存到一个1D纹理里,Shader里直接查表。帧率瞬间恢复。
GPU渲染优化,说白了就是「少干活」。少画没用的像素,少做没用的计算,少采没用的纹理。你让GPU干得越少,它就越流畅。
这张图总结了我们这一章的核心流程。从检测过度绘制开始,到三种优化手段,最终达到性能提升。每一步都有对应的工具和技巧,关键是要养成习惯——每次写完UI,先开过度绘制检测看一眼,别等到用户反馈卡顿了才去查。
本章核心要点:
- 过度绘制检测是GPU优化的第一步,开发者选项里就能开
- 消除过度绘制从「减少背景层数」开始,往往一行代码就能解决大问题
- ClipRect是硬件加速的好搭档,但要注意避开clipPath这个坑
- Shader优化要记住「少干活」三字诀:少分支、少循环、少采样
好了,这一章的内容就到这里。GPU渲染优化是个细活,但也是最容易看到效果的地方。你按我说的这几个方向去排查,大概率能解决80%的渲染性能问题。