4. 过度绘制检测与消除:用开发者工具揪出 Overdraw

Overdraw,说白了就是同一块像素被反复涂了好几遍。你想想看,屏幕上的每个像素最终只能显示一种颜色,但 GPU 可能在背后已经把它画了三四次。这就像刷墙——底漆还没干就刷面漆,浪费涂料不说,还慢。

我刚开始做界面移植时,总觉得 Overdraw 是个小问题。直到有一次,一个看似简单的列表页面,在低端设备上滑动卡成 PPT。打开调试工具一看,好家伙,有些区域被绘制了 5 遍。从那以后,我养成了一个习惯:任何界面改动,先开 Overdraw 检测看一眼。

4.1 什么是过度绘制?

先看一个直观的例子。假设你有一个 Activity:

<!-- 一个典型的 Overdraw 场景 -->
<FrameLayout android:background="@color/white">
    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/blue" />
</FrameLayout>

这段代码里,FrameLayout 先画了一层白色背景,然后 TextView 又画了一层蓝色背景。底层的白色完全被遮挡,白画了。这就是一次典型的 Overdraw。

核心概念:Overdraw 指一个像素在单帧内被绘制多次。理想情况是 1x(每个像素只画一次),2x 以内可接受,超过 3x 就需要警惕了。

4.2 如何检测 Overdraw?

Android 开发者选项里自带了一个神器——「调试 GPU 过度绘制」。打开路径:

  1. 进入「设置」→「开发者选项」
  2. 找到「调试 GPU 过度绘制」
  3. 选择「显示过度绘制区域」

开启后,屏幕会变成一幅「热力图」。颜色越红,Overdraw 越严重:

颜色 含义 建议
无色/蓝色 1x 绘制(理想) 保持现状
绿色 2x 绘制 可接受,但留意
淡红 3x 绘制 需要优化
深红 4x+ 绘制 必须修复

我的习惯:每次提交代码前,我都会在真机上跑一遍 Overdraw 检测。模拟器不准,真机才真实。尤其是那些带渐变、圆角、阴影的控件,最容易出问题。

4.3 常见的 Overdraw 来源

根据我的经验,Overdraw 主要来自这几个地方:

  • 窗口背景 + 布局背景 + 控件背景:三层背景叠在一起,最底层全白画。
  • ListView/RecyclerView 的 item 背景:每个 item 都设背景,列表滚动时反复绘制。
  • 自定义 View 的 onDraw 里重复绘制:比如先画全屏底色,再画内容,最后画边框。
  • Fragment 切换时的重叠:旧 Fragment 没及时移除,新 Fragment 叠在上面。

嗯,这里要特别提一下 Fragment 的问题。我曾经接手过一个项目,主页有 5 个 Tab,每个 Tab 都是一个 Fragment。用户切换时,旧的 Fragment 只是隐藏(hide),并没有移除(remove)。结果呢?5 个 Fragment 的背景全部叠加在一起,深红一片。修复后,帧率从 20fps 直接跳到 55fps。

4.4 消除 Overdraw 的实战技巧

下面是我总结的几个最有效的消除方法:

4.4.1 干掉多余的背景

这是最直接、效果最明显的一招。检查你的布局层级:

<!-- 优化前:三层背景 -->
<!-- Activity 主题自带白色背景 -->
<LinearLayout android:background="@color/white">
    <TextView android:background="@color/white" />
</LinearLayout>

<!-- 优化后:只保留一层 -->
<!-- Activity 主题自带白色背景,布局和控件不再重复设置 -->
<LinearLayout>
    <TextView />
</LinearLayout>

注意:不要盲目删除所有背景。如果某个控件需要背景来区分状态(比如点击态),那就保留。优化的目标是「消除被完全遮挡的绘制」,不是「删光所有背景」。

4.4.2 使用 merge 标签减少布局层级

布局层级越深,Overdraw 风险越大。用 <merge> 可以减少一层不必要的 ViewGroup:

<!-- 优化前 -->
<LinearLayout>
    <FrameLayout>
        <TextView />
    </FrameLayout>
</LinearLayout>

<!-- 优化后 -->
<LinearLayout>
    <merge>
        <TextView />
    </merge>
</LinearLayout>

4.4.3 自定义 View 中避免重复绘制

如果你在写自定义 View,记住一个原则:能用 clipRect 就别画全屏。比如:

@Override
protected void onDraw(Canvas canvas) {
    // 优化前:先画整个背景
    canvas.drawColor(Color.WHITE);
    
    // 优化后:只裁剪需要绘制的区域
    canvas.clipRect(left, top, right, bottom);
    // 然后只在这个区域内绘制
    canvas.drawBitmap(bitmap, left, top, null);
}

4.4.4 使用 ViewStub 延迟加载

那些不一定会显示的布局,用 ViewStub 延迟加载。比如:

<ViewStub
    android:id="@+id/stub_ads"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout="@layout/layout_ads" />

只有当你调用 stub.inflate() 时,这个布局才会被创建。否则它不占任何绘制资源。

4.5 用 SVG 理解 Overdraw 的检测与消除流程

下面这张图,是我自己梳理的 Overdraw 排查流程。每次遇到性能问题,我就按这个步骤走一遍:

Overdraw 检测与消除流程图 Overdraw 检测与消除流程 开启 GPU Overdraw 调试 观察屏幕颜色分布 判断 Overdraw 严重程度 轻度(绿色):记录观察 重度(红色):立即修复 排查布局层级与背景 移除多余背景/层级 优化后再次开启调试验证,直到屏幕以蓝色/无色为主

4.6 避坑指南

最后,分享几个我踩过的坑:

  • 不要只看模拟器:模拟器的 GPU 模拟和真机差异很大。我在模拟器上看全是蓝色,一上真机就红了。后来只用真机调试。
  • 注意主题背景:很多 App 的主题里默认带了 android:windowBackground。如果你在布局里又设了背景,就多了一层。我习惯在主题里把 windowBackground 设为 @null,然后在布局里统一控制。
  • RecyclerView 的 item 背景:不要在 item 根布局设背景,而是用 android:foreground 或者通过 setBackground 动态设置。否则列表滚动时,每个 item 都会触发一次背景绘制。
  • Fragment 重叠:用 replace() 代替 add() + hide(),除非你有特殊需求。否则 Fragment 堆叠起来,Overdraw 指数级增长。

我曾经在一个电商项目里,发现商品详情页的 Overdraw 高达 5x。排查了半天,发现是第三方广告 SDK 在页面最底层画了一个全屏透明背景。联系 SDK 方去掉后,Overdraw 直接降到 1.5x。所以,第三方库也要检查。

Overdraw 检测这件事,说白了就是「用眼睛看,用脑子想」。打开调试工具,扫一眼屏幕,哪里红就修哪里。修完再扫一眼,直到屏幕基本变蓝。这个过程熟练了,五分钟就能搞定一个页面。