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 过度绘制」。打开路径:
- 进入「设置」→「开发者选项」
- 找到「调试 GPU 过度绘制」
- 选择「显示过度绘制区域」
开启后,屏幕会变成一幅「热力图」。颜色越红,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 排查流程。每次遇到性能问题,我就按这个步骤走一遍:
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 检测这件事,说白了就是「用眼睛看,用脑子想」。打开调试工具,扫一眼屏幕,哪里红就修哪里。修完再扫一眼,直到屏幕基本变蓝。这个过程熟练了,五分钟就能搞定一个页面。