EN

Android 性能优化之过度绘制 - 理论篇

字数统计: 2.8k阅读时长: 11 min
2014/10/20
loading

2026-10-13 更新:本文原写于 2014 年,源码节选已对照 AOSP android-4.4.4_r2。Overdraw 的概念仍适用,ADT、monitor、Hierarchy Viewer 和旧 OpenGL 跟踪入口已过时;Android Studio 3.1 起改用 Layout Inspector 查看层级,帧时序使用 Perfetto。下文保留历史源码,并补充 Android 12 / Android 16 的对应边界。

博客有一段时间没有更新了,到了新公司后,一直比较忙,博客也更新地不那么频繁了。最近一直在看Android上和性能相关的部分,也就是所谓的Android性能优化,才发现Android性能这一块,自己懂得还是太少了,所以从上层开始看,也算是一点一点入门吧。这个系列将讲解学习过程中总结的和性能相关的内容。

先讲讲 GPU 过度绘制,也是开发者最直接接触的部分吧。这个内容分两篇来讲:第一篇讲原理和一些优化建议,第二篇用实际例子来看一般的优化步骤。

过度绘制概念

GPU 过度绘制(Overdraw)指同一帧内,一个像素被绘制多次。例如 TextView 先画背景再画文字,文字覆盖到的像素就可能参与两次绘制。多余的像素着色、混合和内存访问会消耗资源,但是否造成掉帧,取决于绘制面积、透明度、GPU 能力以及这一帧的其他工作,不能仅凭颜色数量判断。Android 过度绘制说明

现在排查可以先用开发者选项中的 Debug GPU Overdraw 找到重复绘制区域,再用 Layout Inspector 对照实际层级和背景。Compose 的重组次数与像素过度绘制是不同指标,不能互相代替。

修改后,在相同设备、刷新率和操作下关闭调试着色再比较帧时序。Android 12(API 31)及以上可使用 Perfetto FrameTimeline 对照应用帧和 SurfaceFlinger 帧;该数据源目前不支持 SurfaceView。应用内部少画一层背景,并不自动证明 SurfaceFlinger 合成耗时也下降了。

过度绘制的原因

  1. 多个背景或自定义绘制覆盖了同一块区域,其中一部分完全被不透明内容遮住。
  2. 半透明内容需要与下层像素混合,不能简单跳过被它覆盖的绘制。
  3. 同一绘制过程重复提交了没有可见贡献的图形。

View 数量、嵌套深度和 inflate 时间需要另外分析。一个没有背景也不绘制内容的父容器会增加布局工作,但不一定增加像素过度绘制;一个扁平的自定义 View 也可以在同一像素上画很多次。

过度绘制和不合理的 XML 布局的影响

  1. inflate 读取编译后的布局资源,创建 View 并设置属性。这主要消耗 CPU;节点数量、控件构造和属性处理都会影响耗时,不等于 GPU 已经绘制了多少层。
  2. 布局变化可能触发 measure/layout,界面更新还要记录和执行绘制命令。并非每一帧都重新 inflate 或重新执行完整布局;需要从 trace 中确认实际耗时发生在创建、测量、布局、绘制还是等待阶段。

过度绘制的一些基本概念

在支持的设备上打开「开发者选项 → 调试 GPU 过度绘制 → 显示过度绘制区域」。原文的魅族拨号代码和菜单位置属于当年的机型,不作为现在启用开发者选项的通用步骤。这里主要观察 HWUI 绘制的应用窗口,不能把颜色图当作整机所有 Surface 的完整 GPU 计数器。

默认配色中的次数要区分「总绘制次数」和「额外绘制次数」。以本文核对的 Android 4.4.4 实现为例:

颜色 同一像素的总绘制次数 超过首次绘制的次数
原色,无调试着色 1 0
蓝色 2 1
绿色 3 2
淡红色 4 3
红色 5 及以上 4 及以上

蓝色也有过度绘制。我当时给的检查建议是「控制在 2x、不允许 4x、大面积 3x 不超过四分之一屏幕」,这不是 Android 的通用验收标准。颜色可以帮助找可疑区域,值不值得改,还要看帧耗时和视觉需求;文字、圆角、阴影和半透明效果可能需要叠加绘制。

优化工具介绍

1. Lint 工具

Android Studio 的 lint 可以检查布局和代码中的问题,也可以通过项目的 Gradle lint 任务运行。编辑器中的提示不代表完整 lint 任务已经执行,具体规则和严重级别以项目的 Android Gradle Plugin / lint 版本与配置为准。参见官方 lint 使用说明。

下面保留的是早期工具截图,不是当前 Android Studio 的界面:

Lint

2. Lint 布局建议及边界

  • Compound drawables:如果图片与文本不需要独立的布局、点击或无障碍语义,可以用 TextView 的 compound drawable 减少节点。
  • Merge root frame:仅当根容器没有需要保留的背景、padding、布局或交互行为,并且有合适的宿主父容器时,才考虑 <merge>。它不是可独立实例化的 View。
  • Useless leaf / parent:先确认节点没有自定义绘制、前景、点击、动画或无障碍作用。没有背景本身不足以证明节点可以删除。
  • Deep layouts:嵌套可能增加测量与布局开销。官方布局优化文档列出的默认深度警告阈值为 10,项目可以受版本和配置影响;它是静态检查线索,不是超过 10 层就一定掉帧的结论。

这些规则主要面向 View/XML。修改前后还要检查布局结果是否一致,不能为了消除警告改变界面语义。优化布局层级

3. Hierarchy Viewer 与 Layout Inspector

Hierarchy Viewer 已停止维护。它曾在 Android Device Monitor 中展示视图树及 measure/layout/draw 的相对耗时;旧版能否连接设备取决于系统构建、调试能力和连接协议,不是磁盘是否加密。原文通过 ViewServer 适配旧工具的做法仅供历史参考。

Android Studio 3.1 及以上使用 Layout Inspector 检查运行时层级和属性。它不能替代帧性能测量;定位耗时仍需要 system trace、帧指标或应用自己的测量数据。

布局优化建议

  • 简单横向或纵向布局可以用 LinearLayout;复杂 View 布局可以比较 ConstraintLayout 等方案。不要仅按容器类名给性能排序,也不要把 View/XML 的测量经验直接套到 Compose。
  • 用 <include> 复用布局;满足宿主要求时,用 <merge> 去掉没有行为的中间容器。
  • 对首次显示时不需要的内容,用 ViewStub 延后创建。已经存在的 View 只切换可见性,不需要再次创建对象;因此「动态 inflate 一定比 setVisibility 快」不成立。ViewStub 会把工作推迟到首次展开时,并非消除这些工作。
  • 删除确定没有可见贡献的背景。只在上层始终不透明且完整覆盖下层的区域才安全;还要检查列表空白、滚动边缘、加载、错误、点击和转场状态。
  • selector 的 normal 状态可以在视觉允许时不绘制背景,但不能一律改成透明,也不能同时丢掉选中、按压和无障碍反馈。
  • 嵌套使用 layout_weight 的 LinearLayout 可能需要多轮测量。这是 measure 阶段的成本,不等于所有子 View 都必定在 draw 阶段执行两次;结合 MeasureSpec 与实际 trace 判断。
  • 减少无意义的节点和重复测量,同时保留控件语义。优化目标是降低已测出的成本,而不是让层级图看起来最扁平。

源码相关

有兴趣看源码的同学,可以接着看看 AOSP android-4.4.4_r2 的 OpenGLRenderer.cpp。下面是两段节选,依赖 HWUI 内部状态:renderOverdraw() 把 stencil 计数转换成调试颜色,countOverdraw() 读取调试结果并计算均值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
void OpenGLRenderer::renderOverdraw() {
if (mCaches.debugOverdraw && getTargetFbo() == 0) {
const Rect* clip = &mTilingClip;

mCaches.enableScissor();
mCaches.setScissor(clip->left, mFirstSnapshot->height - clip->bottom,
clip->right - clip->left, clip->bottom - clip->top);

// 1x overdraw
mCaches.stencil.enableDebugTest(2);
drawColor(mCaches.getOverdrawColor(1), SkXfermode::kSrcOver_Mode);

// 2x overdraw
mCaches.stencil.enableDebugTest(3);
drawColor(mCaches.getOverdrawColor(2), SkXfermode::kSrcOver_Mode);

// 3x overdraw
mCaches.stencil.enableDebugTest(4);
drawColor(mCaches.getOverdrawColor(3), SkXfermode::kSrcOver_Mode);

// 4x overdraw and higher
mCaches.stencil.enableDebugTest(4, true);
drawColor(mCaches.getOverdrawColor(4), SkXfermode::kSrcOver_Mode);

mCaches.stencil.disable();
}
}

void OpenGLRenderer::countOverdraw() {
size_t count = mWidth * mHeight;
uint32_t* buffer = new uint32_t[count];
glReadPixels(0, 0, mWidth, mHeight, GL_RGBA, GL_UNSIGNED_BYTE, &buffer[0]);

size_t total = 0;
for (size_t i = 0; i < count; i++) {
total += buffer[i] & 0xff;
}

mOverdraw = total / float(count);

delete[] buffer;
}

glReadPixels() 是这个旧调试计数实现的一部分,不是应用应在每帧额外执行的优化步骤,也不代表 GPU 毫秒耗时。

原文提到的数值叠层位于同一标签的 frameworks/base/core/java/android/view/HardwareRenderer.java。下面保留 debugOverdraw() / drawOverdrawCounter() 节选;AOSP android-5.0.0_r1 的该文件已没有这两个方法,不能在当前系统照搬这个入口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
private void debugOverdraw(View.AttachInfo attachInfo, Rect dirty,
HardwareCanvas canvas, DisplayList displayList) {

if (mDebugOverdraw == OVERDRAW_TYPE_COUNT) {
if (mDebugOverdrawLayer == null) {
mDebugOverdrawLayer = createHardwareLayer(mWidth, mHeight, true);
} else if (mDebugOverdrawLayer.getWidth() != mWidth ||
mDebugOverdrawLayer.getHeight() != mHeight) {
mDebugOverdrawLayer.resize(mWidth, mHeight);
}

if (!mDebugOverdrawLayer.isValid()) {
mDebugOverdraw = -1;
return;
}

HardwareCanvas layerCanvas = mDebugOverdrawLayer.start(canvas, dirty);
countOverdraw(layerCanvas);
final int restoreCount = layerCanvas.save();
layerCanvas.drawDisplayList(displayList, null, DisplayList.FLAG_CLIP_CHILDREN);
layerCanvas.restoreToCount(restoreCount);
mDebugOverdrawLayer.end(canvas);

float overdraw = getOverdraw(layerCanvas);
DisplayMetrics metrics = attachInfo.mRootView.getResources().getDisplayMetrics();

drawOverdrawCounter(canvas, overdraw, metrics.density);
}
}

private void drawOverdrawCounter(HardwareCanvas canvas, float overdraw, float density) {
final String text = String.format("%.2fx", overdraw);
final Paint paint = setupPaint(density);
// HSBtoColor will clamp the values in the 0..1 range
paint.setColor(Color.HSBtoColor(0.28f - 0.28f * overdraw / 3.5f, 0.8f, 1.0f));

canvas.drawText(text, density * 4.0f, mHeight - paint.getFontMetrics().bottom, paint);
}

这两段代码说明 Android 4.4.4 的实现,不能作为现代 HWUI 源码路径。以 AOSP Android 16 的 android-16.0.0_r1 为例,调试过度绘制已经由 SkiaPipeline.cpp 中的 Properties::debugOverdraw 分支调用 renderOverdraw()。阅读设备源码时先固定对应标签,再追踪实际渲染后端。

参考文章

  1. 原优化案例(历史参考,本次未能重新访问)
  2. 反编译并添加gpu显示
  3. https://developer.android.com/develop/ui/views/layout/improving-layouts/optimizing-layouts
  4. https://developer.android.com/develop/ui/views/layout/improving-layouts/reusing-layouts
  5. https://developer.android.com/develop/ui/views/layout/improving-layouts/loading-ondemand
  6. RecyclerView 文档(原 Smooth Scrolling 链接已重定向至此)
  7. https://developer.android.com/studio/profile/hierarchy-viewer
  8. https://developer.android.com/studio/write/lint

关于我 && 博客

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 博主个人介绍 :里面有个人的微信和微信群链接。
  2. 本博客内容导航 :个人博客内容的一个导航。
  3. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  4. Android性能优化知识星球:付费社群已于 2026 年停止运营,不再招收新成员;此处保留历史介绍。

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

CATALOG
  1. 1. 过度绘制概念
  2. 2. 过度绘制的原因
  3. 3. 过度绘制和不合理的 XML 布局的影响
  4. 4. 过度绘制的一些基本概念
  5. 5. 优化工具介绍
    1. 5.1. 1. Lint 工具
    2. 5.2. 2. Lint 布局建议及边界
    3. 5.3. 3. Hierarchy Viewer 与 Layout Inspector
  6. 6. 布局优化建议
  7. 7. 源码相关
  8. 8. 参考文章
  • 关于我 && 博客