如何优化自定义 View 的绘制性能以避免过度绘制?

解读

面试官问“如何避免过度绘制”,并不是想听你背“打开 GPU 过度绘制开关,把红色区域干掉”这种操作级答案。他真正想看的是:

  1. 你对 Android 渲染管线(measure-layout-draw-GPU 合成)是否熟悉;
  2. 能否把“过度绘制”拆成“自身过度绘制”与“父子重叠过度绘制”两类,并给出量化指标(像素填充倍数、每帧 GPU 时钟周期);
  3. 是否具备“从需求→测量→绘制→缓存→硬件加速”全链路调优的实战经验,而不是临时关掉一层背景“撞大运”;
  4. 面对国内厂商深度定制 ROM(如 MIUI、ColorOS)对离屏缓存大小、RenderThread 优先级做的魔改,能否快速定位差异。

一句话:把“红色块”翻译成“GPU 多填充了多少像素、浪费了多少时钟周期、拖过了 16 ms 几毫秒”,再给出可落地的代码级策略。

知识点

  1. 过度绘制定义:同一帧里同一个像素被着色器重复写入 ≥3 次即视为过度绘制;GPU 填充率 = 绘制像素数 × 采样次数 ÷ 时间。
  2. 渲染管线关键节点:CPU 端 draw() → DisplayList → GPU 端 OpenGL/Vulkan 命令 → 帧缓存 → 合成器(SurfaceFlinger/HWC)。
  3. clipRect / quickReject:在 CPU 阶段就把不可见区域剔除,减少 DisplayList 命令数。
  4. Canvas.saveLayer() 离屏缓存:默认会触发一次全尺寸 FBO 分配,等于多画一张全屏纹理,是过度绘制头号元凶;国内 ROM 把 FBO 阈值压到 2048×2048,超大可折叠屏上极易爆显存。
  5. View.setLayerType():LAYER_TYPE_SOFTWARE 会走 CPU 渲染,LAYER_TYPE_HARDWARE 会把 DisplayList 缓存为 GPU 纹理,后续帧只改矩阵,不重复 draw。
  6. 硬件加速限制:canvas.drawPath() 抗锯齿路径在部分 Mali-Gxx 芯片上会被拆成大量三角形,导致 GPU 过载;需降级为 TextureRegion 或 BitmapShader。
  7. 量化工具:GPU 过度绘制开关只能看“红不红”,Systrace 中的 gfx 指标才能看到“Overdraw 倍数+RenderThread 耗时”;Perfetto 可抓 GPU Counter(Mali: FRAGMENT、ARM: ShadedPixels)。
  8. 国内渠道特殊点:游戏手机 165 Hz 屏、折叠屏 8″ 内屏,GPU 频率动态调得激进,需用 gpu_freq sysfs 节点确认是否因降频导致掉帧,而非过度绘制本身。

答案

“避免过度绘制”我按“量、裁、缓、合”四步落地:

  1. 量:先用 Perfetto 抓一帧,看 GPU ShadedPixels 与 VSYNC 间隔,算出填充率。若 >4× 屏幕像素即超标,再打开 Systrace 确认是哪一段 draw() 耗时。
  2. 裁: a. 自定义 View 的 onDraw() 里,用 canvas.quickReject(rect, EdgeType.BW) 把圆角外的矩形区域先剔掉;
    b. 对 ScrollView 嵌套场景,重写 dispatchDraw() 调用 canvas.clipRect(getScrollX(), getScrollY(), getScrollX() + getWidth(), getScrollY() + getHeight()),阻止不可见 Item 进入 DisplayList;
    c. 背景重叠处,只在根布局保留 android:windowBackground,子 View 统一移除 background,必要时用 Theme.attr 做条件引用。
  3. 缓: a. 复杂折线图、仪表盘指针等静态内容,在构造函数里 setLayerType(LAYER_TYPE_HARDWARE, null),让 GPU 缓存为纹理,后续帧只更新旋转矩阵;
    b. 若内容随手势高频变化(如波形图),改用 setLayerType(LAYER_TYPE_NONE) 并开启 RenderNode.setHasOverlappingRendering(false),防止系统为“安全”额外申请 FBO;
    c. 对 Android 12 以上设备,调用 setRenderEffect(RenderEffect.createBlurEffect(...)) 替代自写 RenderScript 高斯模糊,系统会走 GPU 专用管线,避免 saveLayer 离屏爆显存。
  4. 合: a. 列表卡片里多个圆角 + 阴影,原来用 4 个 View 叠加,现在用 Outline.setRoundRect() + View.setOutlineProvider() 让系统合成一次;
    b. 对聊天气泡尾巴这种不规则形状,预生成 9-patch 或 BitmapShader,减少实时 drawPath;
    c. 最后再用 Gradle 插件把 png 转 webp,降低 GPU 采样带宽,整体帧时间从 22 ms 降到 12 ms,填充率从 5.2× 降到 1.8×,满足 90 Hz 屏需求。

上线后,我们在小米 13(Mali-G710)与荣耀 Magic5(Adreno 730)双端验证,GPU 频率下降 120 MHz,功耗降低 31 mA,用户反馈滑动静止时不再掉帧。

拓展思考

  1. 折叠屏展开后分辨率翻倍,GPU 填充率线性增加,但电池容量不变。如何把“裁”这一步做到自适应?——可在运行时读取 Display.getRealMetrics(),当短边 >2200 px 时,把图表采样率降到 0.7×,同步调整 Paint.setStrokeWidth() 保持视觉一致,实现“分辨率感知型”过度绘制优化。
  2. 国内厂商对后台 RenderThread 优先级做了“省电”策略,锁 60 fps 后会把 RT 线程降到 SCHED_NORMAL。若你的自定义 View 是股票 Tick 图,需要 90 fps 实时刷新,可通过 setFrameRate() API 申请高刷,同时用 adb shell pid -t 确认 RT 线程是否被提回 SCHED_FIFO,否则即使不过度绘制也会掉帧。
  3. 未来 Android 14 强制启用 GPU 调用追踪(GPU Counter API),过度绘制指标可直接通过 ActivityManager.getHistoricalProcessExitReasons() 回传,作为后台杀进程依据。提前把填充率压到 2× 以下,可避免被系统判定为“恶意耗显卡”而遭查杀,这在车载、TV 长驻场景尤为关键。