3、渲染性能瓶颈分析:过度绘制(Overdraw)检测与优化、GPU 渲染压力分析、CPU 与 GPU 负载均衡、使用 Systrace 和 Perfetto 定位问题

各位同学,欢迎来到第三章。前两章我们聊了渲染管线和硬件加速,算是把「理论地基」打好了。这一章,咱们直接上战场——分析性能瓶颈。

说实话,车载系统的渲染优化,跟手机端有很大不同。手机卡了,用户顶多骂一句「破手机」。车机卡了,那可是要出大事的。我见过一个项目,倒车影像启动慢了 200 毫秒,客户直接拒收。所以,这一章的内容,你不仅要懂,还得刻在脑子里。

3.1 过度绘制(Overdraw)检测与优化

过度绘制,说白了就是「重复画」。你想想看,一个像素点,本来画一次就够了,结果因为布局嵌套、背景重复设置,GPU 硬是给它画了三四遍。这不是浪费算力是什么?

在 Android 上,检测 Overdraw 最直接的办法就是打开「调试 GPU 过度绘制」开关。我习惯在开发者选项里直接开启,然后整个屏幕就会变成一张「热力图」:

  • 蓝色:没有过度绘制(理想状态)
  • 绿色:绘制了 1 次(可以接受)
  • 粉色:绘制了 2-3 次(需要关注)
  • 红色:绘制了 4 次以上(必须优化)

我在项目中遇到过最夸张的一次,一个简单的列表项,红色区域占了 60%。查了半天,发现是每个 item 都设置了背景色,父布局又设了一层背景,再加上系统默认的窗口背景……三层叠加,GPU 直接「罢工」。

优化三板斧:

  1. 移除不必要的背景:父布局有背景,子布局就别再设了。尤其是 windowBackground,很多人忘了关。
  2. 使用 mergeViewStub:减少布局层级,从根源上避免重复绘制。
  3. 自定义 View 的 onDraw() 里用 clipRect():告诉 GPU「这块不用画」,直接跳过。

小技巧:我曾经在优化一个仪表盘界面时,用 canvas.clipRect() 把不需要刷新的区域裁剪掉,帧率直接从 45fps 飙到 60fps。你试试看,效果立竿见影。

3.2 GPU 渲染压力分析

Overdraw 是 GPU 压力的一个方面,但不是全部。GPU 渲染压力,更多体现在「着色器复杂度」和「纹理带宽」上。

怎么分析?用 dumpsys gfxinfo 或者 Profile GPU Rendering 工具。我个人习惯用后者,因为它能直观地看到每一帧的绘制时间:

  • 水平绿线:16ms 基准线(60fps 的底线)
  • 蓝色柱状图:表示创建/更新 DisplayList 的时间
  • 红色柱状图:表示执行绘制命令的时间
  • 黄色柱状图:表示 CPU 等待 GPU 完成的时间

如果红色柱状图经常超过绿线,说明 GPU 压力过大。这时候,你需要检查:

  • 是否使用了复杂的 Shader:比如在 Fragment Shader 里做大量数学运算。
  • 纹理是否过大:一张 4K 的图片缩放到 100x100 显示,GPU 照样要加载整张纹理。浪费带宽。
  • 是否频繁切换纹理:每帧切换几十次纹理,GPU 的缓存根本来不及反应。

避坑指南:我曾经接手过一个导航项目,地图渲染时 GPU 占用率飙到 90%。查了半天,发现是地图瓦片纹理没有做 mipmap。加上 android:hardwareAccelerated="true" 和 mipmap 后,GPU 占用直接降到 40%。嗯,细节决定成败。

3.3 CPU 与 GPU 负载均衡

很多开发者只盯着 GPU,却忽略了 CPU。其实,渲染瓶颈往往出在 CPU 端。为什么?因为 CPU 负责「准备数据」,GPU 负责「执行绘制」。如果 CPU 准备数据太慢,GPU 就得干等着。

我见过一个典型案例:一个列表滑动卡顿,GPU 压力并不大,但 CPU 的 measurelayout 耗时特别长。说白了,就是布局太复杂,每次滑动都要重新测量。

怎么做到负载均衡?我的经验是:

  • 减少 CPU 端的布局计算:使用 ConstraintLayout 减少嵌套,或者用 RecyclerViewsetHasFixedSize(true) 避免重复测量。
  • 把计算任务移到 GPU:比如用 RenderScriptOpenGL 处理图像,而不是在 CPU 上逐像素操作。
  • 异步化:不要在 UI 线程做耗时操作。我习惯用 AsyncTaskCoroutine 把数据预处理放到后台。

一个实用的判断方法:用 Systrace 看 Choreographer#doFrame 的耗时。如果 InputAnimationTraversal 三个阶段加起来超过 16ms,那 CPU 就是瓶颈。如果 Draw 阶段耗时过长,那 GPU 才是问题。

3.4 使用 Systrace 和 Perfetto 定位问题

好了,理论讲完了,咱们来点实战工具。Systrace 和 Perfetto,是 Android 性能分析的「倚天剑」和「屠龙刀」。

3.4.1 Systrace:老牌工具,依然能打

Systrace 是 Android 自带的性能分析工具。我习惯在命令行里用:

python systrace.py -t 10 -o trace.html gfx input view webview

这个命令会抓取 10 秒的 trace,重点关注 gfx(图形)、input(输入)、view(视图)和 webview 这几个标签。

打开生成的 trace.html,你会看到一条条彩色「泳道」。每个泳道代表一个进程或线程。我最关注的是:

  • SurfaceFlinger:负责合成所有图层。如果这里出现掉帧,说明图层太多或者合成太慢。
  • RenderThread:负责执行 GPU 命令。如果这里出现长时间空白,说明 CPU 在等 GPU。
  • 主线程:看 doFrame 事件。如果出现红色标记,说明这一帧超时了。

我的习惯:抓 trace 时,我会故意触发卡顿场景(比如快速滑动列表),然后看 trace 上有没有「断层」或「红色标记」。有一次,我发现 SurfaceFlingeronFrameAvailable 回调里有个 Binder 调用耗时 50ms,查下去发现是某个服务进程在同步数据。去掉这个同步后,卡顿消失了。

3.4.2 Perfetto:新一代性能分析神器

Perfetto 是 Systrace 的升级版,功能更强大,界面更友好。我建议你直接用它,尤其是分析 GPU 问题时。

抓取 Perfetto trace 的命令:

adb shell perfetto -o /data/misc/perfetto-traces/trace.perfetto-trace -t 10s sched gpu freq

然后打开 https://ui.perfetto.dev,把 trace 文件拖进去。你会看到:

  • GPU 频率和负载:直接显示 GPU 的实时频率和占用率。如果频率一直跑在最高档,说明 GPU 压力大。
  • 帧边界:Perfetto 会自动标记每一帧的开始和结束。如果两帧之间的间隔超过 16ms,那就是掉帧。
  • CPU 调度:可以看到每个线程在哪个 CPU 核心上运行。如果渲染线程被调度到小核上,性能肯定上不去。

避坑指南:我曾经用 Perfetto 分析一个车载系统的启动动画卡顿问题。发现 RenderThread 被调度到了 little core(小核)上,而 big core(大核)却在空闲。后来在 cgroup 里把渲染线程绑定到大核上,问题解决。你看,有时候不是代码的问题,是调度的问题。

3.5 实战案例:一个典型的 Overdraw 优化过程

最后,我分享一个真实案例。某款车型的空调控制界面,滑动温度条时掉帧严重。我用 Systrace 抓了一下,发现 doFrame 耗时 25ms,其中 Draw 阶段占了 18ms。

打开「调试 GPU 过度绘制」,整个界面一片粉红。仔细一看,温度条的背景、滑块、文字,每个 View 都设置了背景色,而且父布局还有一个半透明遮罩层。

优化步骤:

  1. 移除所有子 View 的背景,只保留父布局的背景。
  2. 把半透明遮罩层改为 clipRect 裁剪,而不是叠加一个 View。
  3. 温度条的滑块改用 Bitmap 缓存,避免每次重绘。

优化后,Draw 阶段降到 5ms,帧率稳定在 60fps。客户测试后,直接签字验收。

总结一句话:渲染优化,先看 Overdraw,再看 GPU 压力,最后检查 CPU 和 GPU 的负载均衡。工具用 Systrace 或 Perfetto,定位问题要精准,不要盲目优化。

好了,这一章就到这里。下一章,我们会深入「图层合成与 SurfaceFlinger 优化」,敬请期待。