安卓 View 绘制机制深度解析

安卓 View 绘制机制深度解析

View 是安卓 UI 的基石,所有可视化组件(如 TextView、Button)都继承自 View。理解 View 的绘制机制,不仅能解决日常开发中的 UI 卡顿、错位问题,更是安卓面试的核心考点。本文将从 View 体系基础出发,逐层拆解Measure(测量)、Layout(布局)、Draw(绘制) 三大流程,结合源码与实例,带你彻底掌握 View 绘制的底层逻辑。

一、View 体系基础:先搞懂 View 的 "组织结构"

在分析绘制流程前,必须先明确 View 的层级结构 ------ 安卓 UI 是一棵View 树 ,由ViewViewGroup组成:

  • View:最小的 UI 单元,无子 View(如 TextView);

  • ViewGroup:容器类 View,可包含多个子 View(如 LinearLayout、RelativeLayout),负责管理子 View 的测量、布局与绘制。

核心概念:View 的坐标系

View 的位置由 4 个参数决定(单位:像素),所有参数均相对于父 View

  • left:View 左边缘到父 View 左边缘的距离;

  • top:View 上边缘到父 View 上边缘的距离;

  • right:View 右边缘到父 View 左边缘的距离;

  • bottom:View 下边缘到父 View 上边缘的距离;

注意:View 还有 "绝对坐标系"(相对于屏幕左上角),通过getLocationOnScreen(int[] location)获取,但绘制流程中主要依赖 "父 View 坐标系"。

二、View 绘制三大核心流程:Measure → Layout → Draw

安卓系统通过ViewRootImplperformTraversals()方法触发 View 树的绘制,该方法会依次调用performMeasure()performLayout()performDraw(),对应三大流程。流程具有自上而下的特性:父 View 先测量 / 布局 / 绘制自己,再递归处理子 View。

2.1 第一流程:Measure(测量)------ 确定 View 的宽高

核心目标 :计算 View 的measuredWidth(测量宽度)和measuredHeight(测量高度),为后续布局提供依据。

2.1.1 关键角色:MeasureSpec(测量约束)

父 View 通过MeasureSpec向子 View 传递 "测量规则",MeasureSpec是一个 32 位 int 值,由2 位模式(mode)30 位尺寸(size) 组成:

  • 模式(mode):决定子 View 的宽高计算方式;

  • 尺寸(size):父 View 提供的参考尺寸(如父 View 的可用宽度)。

2.1.2 MeasureSpec 的 3 种模式
模式 含义 常见场景
EXACTLY(精确模式) 子 View 的宽高是确定值,父 View 已明确指定子 View 的最终尺寸 子 View 设置match_parent或固定值(如100dp
AT_MOST(最大模式) 子 View 的宽高不能超过父 View 提供的参考尺寸,需自己计算实际需要的尺寸 子 View 设置wrap_content
UNSPECIFIED(无约束) 父 View 不限制子 View 的尺寸,子 View 可自由设置(极少用于普通 View) ScrollView 的子 View、ListView 的 item
2.1.3 Measure 流程源码拆解(以 ViewGroup 为例)
  1. 父 View 生成子 View 的 MeasureSpec

    ViewGroup 通过getChildMeasureSpec(int parentMeasureSpec, int padding, int childDimension)方法,结合自身的MeasureSpec、内边距(padding)和子 View 的layout_width/layout_height属性,生成子 View 的MeasureSpec

    示例逻辑(简化):

java 复制代码
// 父View为LinearLayout,子View的layout_width=wrap_content

int parentWidthSpec = MeasureSpec.makeMeasureSpec(500, MeasureSpec.EXACTLY); // 父View宽500px

int paddingLeft = getPaddingLeft();

int paddingRight = getPaddingRight();

int availableWidth = parentWidthSpec - paddingLeft - paddingRight; // 父View可用宽度

// 子View layout_width=wrap_content → 模式AT_MOST,尺寸=可用宽度

int childWidthSpec = MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST);
  1. 子 View 执行测量

    父 View 调用子 View 的measure(int widthMeasureSpec, int heightMeasureSpec)方法,子 View 在该方法中:

    普通 View(如 TextView)的onMeasure逻辑:

java 复制代码
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

   // 1. 解析父View传递的MeasureSpec

   int widthMode = MeasureSpec.getMode(widthMeasureSpec);

   int widthSize = MeasureSpec.getSize(widthMeasureSpec);

   int heightMode = MeasureSpec.getMode(heightMeasureSpec);

   int heightSize = MeasureSpec.getSize(heightMeasureSpec);

   // 2. 根据模式计算测量宽高

   int measuredWidth;

   int measuredHeight;


   // 宽:EXACTLY → 直接用父View给的size;AT_MOST → 计算文本所需宽度(不超过size)

   if (widthMode == MeasureSpec.EXACTLY) {

       measuredWidth = widthSize;

   } else {

       measuredWidth = calculateTextWidth(); // 自定义方法:计算文本实际宽度

       if (widthMode == MeasureSpec.AT_MOST) {

           measuredWidth = Math.min(measuredWidth, widthSize);

       }

   }


   // 高:逻辑与宽类似

   if (heightMode == MeasureSpec.EXACTLY) {

       measuredHeight = heightSize;

   } else {

       measuredHeight = calculateTextHeight(); // 自定义方法:计算文本实际高度

       if (heightMode == MeasureSpec.AT_MOST) {

           measuredHeight = Math.min(measuredHeight, heightSize);

       }

   }

   // 3. 保存测量结果

   setMeasuredDimension(measuredWidth, measuredHeight);

}
  • 解析MeasureSpec的模式和尺寸;

  • 调用onMeasure(int widthMeasureSpec, int heightMeasureSpec)计算自身的measuredWidthmeasuredHeight

  • 调用setMeasuredDimension(int measuredWidth, int measuredHeight)保存测量结果。

  1. ViewGroup 的特殊处理

    ViewGroup 需先测量所有子 View,再根据子 View 的测量结果计算自身的测量宽高。例如LinearLayoutonMeasure

scss 复制代码
@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

   super.onMeasure(widthMeasureSpec, heightMeasureSpec);

   // 1. 测量所有子View

   measureChildren(widthMeasureSpec, heightMeasureSpec);


   // 2. 根据子View的测量结果计算自身宽高(以垂直方向为例)

   int totalHeight = getPaddingTop() + getPaddingBottom();

   for (int i = 0; i < getChildCount(); i++) {

       View child = getChildAt(i);

       totalHeight += child.getMeasuredHeight(); // 累加子View高度

       totalHeight += getChildSpacing(); // 累加子View间距

   }


   // 3. 保存自身测量结果

   setMeasuredDimension(getMeasuredWidth(), totalHeight);

}
2.1.4 注意点:wrap_content 为什么需要重写 onMeasure?

默认情况下,View 的onMeasurewrap_content的处理与match_parent一致(直接使用父 View 的size),导致wrap_content失效。因此,自定义 View 时必须重写onMeasure,为AT_MOST模式(对应wrap_content)计算实际需要的宽高。

2.2 第二流程:Layout(布局)------ 确定 View 的位置

核心目标 :根据 Measure 流程得到的measuredWidthmeasuredHeight,确定 View 在父 View 中的具体位置(lefttoprightbottom),最终生成width(实际宽度)和height(实际高度)。

2.2.1 Layout 流程源码拆解
  1. 父 View 触发子 View 布局

    ViewGroup 的onLayout(boolean changed, int l, int t, int r, int b)方法是布局的核心,该方法会:

    示例(LinearLayout 垂直布局的onLayout):

java 复制代码
@Override

protected void onLayout(boolean changed, int l, int t, int r, int b) {

   int childLeft = getPaddingLeft(); // 子View的左起点(父View的内边距)

   int childTop = getPaddingTop();  // 子View的上起点


   for (int i = 0; i < getChildCount(); i++) {

       View child = getChildAt(i);

       if (child.getVisibility() == View.GONE) continue;


       // 1. 获取子View的测量宽高

       int childWidth = child.getMeasuredWidth();

       int childHeight = child.getMeasuredHeight();


       // 2. 计算子View的右、下边界

       int childRight = childLeft + childWidth;

       int childBottom = childTop + childHeight;


       // 3. 调用子View的layout方法,确定其位置

       child.layout(childLeft, childTop, childRight, childBottom);


       // 4. 更新下一个子View的上起点(累加当前子View高度和间距)

       childTop = childBottom + getChildSpacing();

   }

}
  • 遍历所有子 View;

  • 计算每个子 View 的lefttoprightbottom

  • 调用子 View 的layout(int l, int t, int r, int b)方法。

  1. 子 View 的 layout 方法

    View 的layout方法会先调用setFrame(int l, int t, int r, int b)保存位置参数,再调用onLayout(空实现,ViewGroup 需重写):

java 复制代码
public void layout(int l, int t, int r, int b) {

   // 1. 保存位置参数(left、top、right、bottom)

   boolean changed = setFrame(l, t, r, b);


   // 2. 如果位置有变化,触发onLayout(ViewGroup在此处布局子View)

   if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {

       onLayout(changed, l, t, r, b);

   }

}
2.2.2 关键区别:getMeasuredWidth () vs getWidth ()
  • getMeasuredWidth():返回 Measure 流程中setMeasuredDimension设置的measuredWidth,是 "测量宽高";

  • getWidth():返回 Layout 流程中right - left的结果,是 "实际显示宽高";

通常情况下两者相等 ,但如果在onLayout中强制修改子 View 的位置(如手动设置right = left + 200),则getWidth()会变为 200,而getMeasuredWidth()仍为测量值。

2.3 第三流程:Draw(绘制)------ 将 View 渲染到屏幕

核心目标 :根据 Layout 流程确定的位置,将 View 的内容(背景、文本、图片等)绘制到屏幕上,核心方法是draw(Canvas canvas)

2.3.1 Draw 流程的 6 个步骤(源码逻辑)

View 的draw方法严格按照以下顺序执行,缺一不可:

java 复制代码
public void draw(Canvas canvas) {

   // 步骤1:绘制View的背景(background)

   drawBackground(canvas);


   // 步骤2:如果需要,保存画布状态(用于后续裁剪、平移等操作)

   if (saveCount > 0) {

       canvas.save();

   }


   // 步骤3:绘制View的自身内容(如TextView的文本、ImageView的图片)

   onDraw(canvas);


   // 步骤4:绘制子View(仅ViewGroup实现,View为空方法)

   dispatchDraw(canvas);


   // 步骤5:绘制装饰内容(如滚动条、前景色、水波纹)
   onDrawForeground(canvas);


   // 步骤6:恢复画布状态(与步骤2对应)

   if (saveCount > 0) {

       canvas.restore();

   }

}
2.3.2 关键方法解析
  1. drawBackground(Canvas canvas)

    绘制background属性设置的 Drawable,会根据 View 的lefttoprightbottom调整 Drawable 的大小。

  2. onDraw(Canvas canvas)

    绘制 View 的核心内容,普通 View(如 TextView)在此方法中绘制文本,自定义 View 需重写此方法实现自定义绘制(如绘制圆形、折线)。

    ❗ 注意:不能在 onDraw 中创建新对象 (如 Paint、Path),否则会频繁触发 GC,导致 UI 卡顿。应将对象定义为成员变量,在initonSizeChanged中初始化。

  3. dispatchDraw(Canvas canvas)

    仅 ViewGroup 实现,用于绘制所有子 View,遍历子 View 并调用其draw方法。绘制顺序与子 View 在 XML 中的顺序一致,后绘制的子 View 会覆盖先绘制的。

三、关键细节与优化:避免 UI 卡顿、错位

3.1 绘制触发机制:3 个核心方法的区别

方法 作用 触发流程 线程限制
requestLayout() 通知 View 树重新测量和布局(宽高或位置变化) Measure → Layout 主线程
invalidate() 通知 View 重新绘制(内容变化,如文本颜色、图片) Draw 主线程
postInvalidate() invalidate()功能一致,用于子线程触发绘制 Draw 子线程

❗ 注意:requestLayout()不会触发Draw流程,invalidate()不会触发MeasureLayout流程。

3.2 硬件加速:提升绘制效率

原理:通过 GPU(图形处理器)替代 CPU 处理绘制任务,将复杂的绘制操作(如纹理渲染)交给 GPU,减少 CPU 负载,提升 UI 流畅度。

3.2.1 开启 / 关闭硬件加速

硬件加速默认开启(API 14+),可在 3 个级别控制:

  1. Application 级别(AndroidManifest.xml):
ini 复制代码
\<application android:hardwareAccelerated="true">
  1. Activity 级别
ini 复制代码
\<activity android:hardwareAccelerated="false">
  1. View 级别(代码中):
csharp 复制代码
view.setLayerType(View.LAYER_TYPE_SOFTWARE, null); // 关闭硬件加速
3.2.2 注意事项

部分 Canvas API 不支持硬件加速,使用后会导致绘制异常(如显示空白、闪烁),需关闭硬件加速。不支持的 API 包括:

  • Canvas.saveLayerAlpha()

  • Canvas.clipPath()(API 18 + 支持,但仍有兼容性问题);

  • Paint.setShadowLayer()(硬件加速下阴影效果可能异常)。

3.3 绘制优化技巧

  1. 避免过度绘制(Overdraw)

    过度绘制指同一像素被多次绘制(如重叠的 View、多层背景),会增加 GPU 负载。可通过「开发者选项 → 调试 GPU 过度绘制」开启过度绘制检测,优化方式:

  • 移除不必要的背景(如父 View 的背景覆盖子 View,可删除子 View 的背景);

  • 使用View.setWillNotDraw(true):如果 ViewGroup 没有自定义绘制内容(不重写onDraw),设置此属性可跳过draw流程,减少绘制步骤;

  • merge标签优化布局:减少冗余的 ViewGroup(如 LinearLayout 嵌套 LinearLayout)。

  1. 减少绘制频率
  • 避免频繁调用invalidate():如需频繁更新 UI(如倒计时),使用postDelayedValueAnimator,减少invalidate()调用次数;

  • 局部刷新:使用invalidate(Rect dirty)invalidate(int l, int t, int r, int b),仅刷新变化的区域,而非整个 View。

  1. 自定义 View 优化
  • 重写onSizeChanged:在 View 尺寸变化时初始化绘制所需的对象(如 Paint、Path),避免在onDraw中创建;

  • 使用clipRect():裁剪画布,只绘制可见区域(如 ListView 的 item,只绘制屏幕内的部分)。

四、高频面试题及解析

1. View 绘制的三大流程是什么?各自的核心作用是什么?

答案

三大流程依次为MeasureLayoutDraw,作用如下:

  1. Measure:通过MeasureSpec约束,计算 View 的measuredWidthmeasuredHeight,确定 View 的测量宽高;

  2. Layout:根据测量宽高,确定 View 在父 View 中的位置(lefttoprightbottom),生成实际显示宽高;

  3. Draw:根据位置和内容,将 View 的背景、核心内容、子 View 等渲染到屏幕上。

2. MeasureSpec 的三种模式分别是什么?对应哪些 layout_width/layout_height 属性?

答案

MeasureSpec 有三种模式,对应关系如下:

  1. EXACTLY(精确模式):子 View 宽高为确定值,对应match_parent或固定尺寸(如100dp);

  2. AT_MOST(最大模式):子 View 宽高不超过父 View 提供的参考尺寸,对应wrap_content

  3. UNSPECIFIED(无约束模式):父 View 不限制子 View 尺寸,对应 ScrollView 的子 View、ListView 的 item 等场景。

3. 自定义 View 时,为什么wrap_content需要重写onMeasure

答案

默认情况下,View 的onMeasure方法对AT_MOST模式(对应wrap_content)的处理逻辑与EXACTLY模式一致,直接使用父 View 提供的size作为测量宽高,导致wrap_content效果等同于match_parent。因此,必须重写onMeasure,为AT_MOST模式计算 View 的实际内容宽高(如文本宽度、图片尺寸),并通过setMeasuredDimension设置,确保wrap_content生效。

4. getMeasuredWidth()getWidth()的区别是什么?

答案

  1. 含义不同:
  • getMeasuredWidth():返回Measure流程中setMeasuredDimension设置的measuredWidth,是 "测量宽高";

  • getWidth():返回Layout流程中right - left的结果,是 "实际显示宽高";

  1. 时机不同:
  • getMeasuredWidth()onMeasure执行后即可获取;

  • getWidth()onLayout执行后才能获取;

  1. 一致性:通常情况下两者相等,但若在onLayout中强制修改 View 的位置(如手动调整right),则getWidth()会变化,getMeasuredWidth()不变。

5. invalidate()requestLayout()的区别是什么?

答案

  1. 作用不同:
  • invalidate():仅通知 View 重新绘制(内容变化,如文本颜色),不触发MeasureLayout流程;

  • requestLayout():通知 View 树重新测量和布局(宽高或位置变化),不触发Draw流程;

  1. 适用场景不同:
  • 文本颜色、图片切换等内容变化,用invalidate()

  • View 宽高、位置变化(如动态修改layout_width),用requestLayout()

6. 为什么不能在onDraw中创建新对象?如何优化?

答案

  • 原因:onDraw会频繁调用(如屏幕刷新、invalidate()触发),每次创建新对象(如 Paint、Path)会导致内存频繁分配与回收,触发 GC(垃圾回收),GC 会阻塞主线程,导致 UI 卡顿;

  • 优化:将绘制所需的对象(如 Paint、Path)定义为 View 的成员变量,在init(构造方法)或onSizeChanged(View 尺寸变化时)中初始化,避免在onDraw中重复创建。

7. SurfaceView 和普通 View 的区别是什么?适用场景有哪些?

答案

  1. 绘制线程不同:
  • 普通 View:在主线程(UI 线程)绘制,若绘制任务复杂(如视频解码、游戏渲染),会阻塞主线程,导致 UI 卡顿;

  • SurfaceView:拥有独立的Surface(绘图缓冲区),可在子线程绘制,避免阻塞主线程;

  1. 绘制机制不同:
  • 普通 View:绘制内容依赖Canvas,与 View 树的绘制流程同步;

  • SurfaceView:绘制内容直接渲染到Surface,通过SurfaceHolder管理绘制,支持双缓冲(减少画面闪烁);

  1. 适用场景:
  • 普通 View:适用于简单 UI(如文本、图片);

  • SurfaceView:适用于复杂绘制场景(如视频播放、游戏、实时摄像头预览)。

五、总结

View 绘制机制是安卓 UI 的核心,从Measure确定宽高,到Layout确定位置,再到Draw渲染内容,每个流程都有严格的逻辑和源码规范。日常开发中,需注意避免过度绘制、减少onDraw中的对象创建,合理使用requestLayoutinvalidate;面试中,需重点掌握三大流程的原理、MeasureSpec的模式、getMeasuredWidthgetWidth的区别等考点。

相关推荐
叽哥2 小时前
Kotlin学习第 9 课:Kotlin 实战应用:从案例到项目
android·java·kotlin
雨白13 小时前
Java 线程通信基础:interrupt、wait 和 notifyAll 详解
android·java
诺诺Okami17 小时前
Android Framework-Launcher-UI和组件
android
潘潘潘18 小时前
Android线程间通信机制Handler介绍
android
潘潘潘18 小时前
Android动态链接库So的加载
android
潘潘潘19 小时前
Android多线程机制简介
android
CYRUS_STUDIO21 小时前
利用 Linux 信号机制(SIGTRAP)实现 Android 下的反调试
android·安全·逆向
CYRUS_STUDIO21 小时前
Android 反调试攻防实战:多重检测手段解析与内核级绕过方案
android·操作系统·逆向
黄林晴1 天前
如何判断手机是否是纯血鸿蒙系统
android