安卓 View 绘制机制深度解析
View 是安卓 UI 的基石,所有可视化组件(如 TextView、Button)都继承自 View。理解 View 的绘制机制,不仅能解决日常开发中的 UI 卡顿、错位问题,更是安卓面试的核心考点。本文将从 View 体系基础出发,逐层拆解Measure(测量)、Layout(布局)、Draw(绘制) 三大流程,结合源码与实例,带你彻底掌握 View 绘制的底层逻辑。
一、View 体系基础:先搞懂 View 的 "组织结构"
在分析绘制流程前,必须先明确 View 的层级结构 ------ 安卓 UI 是一棵View 树 ,由View
和ViewGroup
组成:
-
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
安卓系统通过ViewRootImpl
的performTraversals()
方法触发 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 为例)
-
父 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);
-
子 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)
计算自身的measuredWidth
和measuredHeight
; -
调用
setMeasuredDimension(int measuredWidth, int measuredHeight)
保存测量结果。
-
ViewGroup 的特殊处理
ViewGroup 需先测量所有子 View,再根据子 View 的测量结果计算自身的测量宽高。例如
LinearLayout
的onMeasure
:
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 的onMeasure
对wrap_content
的处理与match_parent
一致(直接使用父 View 的size
),导致wrap_content
失效。因此,自定义 View 时必须重写onMeasure
,为AT_MOST
模式(对应wrap_content
)计算实际需要的宽高。
2.2 第二流程:Layout(布局)------ 确定 View 的位置
核心目标 :根据 Measure 流程得到的measuredWidth
和measuredHeight
,确定 View 在父 View 中的具体位置(left
、top
、right
、bottom
),最终生成width
(实际宽度)和height
(实际高度)。
2.2.1 Layout 流程源码拆解
-
父 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 的
left
、top
、right
、bottom
; -
调用子 View 的
layout(int l, int t, int r, int b)
方法。
-
子 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 关键方法解析
-
drawBackground(Canvas canvas)
绘制
background
属性设置的 Drawable,会根据 View 的left
、top
、right
、bottom
调整 Drawable 的大小。 -
onDraw(Canvas canvas)
绘制 View 的核心内容,普通 View(如 TextView)在此方法中绘制文本,自定义 View 需重写此方法实现自定义绘制(如绘制圆形、折线)。
❗ 注意:不能在 onDraw 中创建新对象 (如 Paint、Path),否则会频繁触发 GC,导致 UI 卡顿。应将对象定义为成员变量,在
init
或onSizeChanged
中初始化。 -
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()
不会触发Measure
和Layout
流程。
3.2 硬件加速:提升绘制效率
原理:通过 GPU(图形处理器)替代 CPU 处理绘制任务,将复杂的绘制操作(如纹理渲染)交给 GPU,减少 CPU 负载,提升 UI 流畅度。
3.2.1 开启 / 关闭硬件加速
硬件加速默认开启(API 14+),可在 3 个级别控制:
- Application 级别(AndroidManifest.xml):
ini
\<application android:hardwareAccelerated="true">
- Activity 级别:
ini
\<activity android:hardwareAccelerated="false">
- 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 绘制优化技巧
-
避免过度绘制(Overdraw)
过度绘制指同一像素被多次绘制(如重叠的 View、多层背景),会增加 GPU 负载。可通过「开发者选项 → 调试 GPU 过度绘制」开启过度绘制检测,优化方式:
-
移除不必要的背景(如父 View 的背景覆盖子 View,可删除子 View 的背景);
-
使用
View.setWillNotDraw(true)
:如果 ViewGroup 没有自定义绘制内容(不重写onDraw
),设置此属性可跳过draw
流程,减少绘制步骤; -
用
merge
标签优化布局:减少冗余的 ViewGroup(如 LinearLayout 嵌套 LinearLayout)。
- 减少绘制频率
-
避免频繁调用
invalidate()
:如需频繁更新 UI(如倒计时),使用postDelayed
或ValueAnimator
,减少invalidate()
调用次数; -
局部刷新:使用
invalidate(Rect dirty)
或invalidate(int l, int t, int r, int b)
,仅刷新变化的区域,而非整个 View。
- 自定义 View 优化
-
重写
onSizeChanged
:在 View 尺寸变化时初始化绘制所需的对象(如 Paint、Path),避免在onDraw
中创建; -
使用
clipRect()
:裁剪画布,只绘制可见区域(如 ListView 的 item,只绘制屏幕内的部分)。
四、高频面试题及解析
1. View 绘制的三大流程是什么?各自的核心作用是什么?
答案:
三大流程依次为Measure
、Layout
、Draw
,作用如下:
-
Measure
:通过MeasureSpec
约束,计算 View 的measuredWidth
和measuredHeight
,确定 View 的测量宽高; -
Layout
:根据测量宽高,确定 View 在父 View 中的位置(left
、top
、right
、bottom
),生成实际显示宽高; -
Draw
:根据位置和内容,将 View 的背景、核心内容、子 View 等渲染到屏幕上。
2. MeasureSpec 的三种模式分别是什么?对应哪些 layout_width/layout_height 属性?
答案:
MeasureSpec 有三种模式,对应关系如下:
-
EXACTLY
(精确模式):子 View 宽高为确定值,对应match_parent
或固定尺寸(如100dp
); -
AT_MOST
(最大模式):子 View 宽高不超过父 View 提供的参考尺寸,对应wrap_content
; -
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()
的区别是什么?
答案:
- 含义不同:
-
getMeasuredWidth()
:返回Measure
流程中setMeasuredDimension
设置的measuredWidth
,是 "测量宽高"; -
getWidth()
:返回Layout
流程中right - left
的结果,是 "实际显示宽高";
- 时机不同:
-
getMeasuredWidth()
在onMeasure
执行后即可获取; -
getWidth()
在onLayout
执行后才能获取;
- 一致性:通常情况下两者相等,但若在
onLayout
中强制修改 View 的位置(如手动调整right
),则getWidth()
会变化,getMeasuredWidth()
不变。
5. invalidate()
和requestLayout()
的区别是什么?
答案:
- 作用不同:
-
invalidate()
:仅通知 View 重新绘制(内容变化,如文本颜色),不触发Measure
和Layout
流程; -
requestLayout()
:通知 View 树重新测量和布局(宽高或位置变化),不触发Draw
流程;
- 适用场景不同:
-
文本颜色、图片切换等内容变化,用
invalidate()
; -
View 宽高、位置变化(如动态修改
layout_width
),用requestLayout()
。
6. 为什么不能在onDraw
中创建新对象?如何优化?
答案:
-
原因:
onDraw
会频繁调用(如屏幕刷新、invalidate()
触发),每次创建新对象(如 Paint、Path)会导致内存频繁分配与回收,触发 GC(垃圾回收),GC 会阻塞主线程,导致 UI 卡顿; -
优化:将绘制所需的对象(如 Paint、Path)定义为 View 的成员变量,在
init
(构造方法)或onSizeChanged
(View 尺寸变化时)中初始化,避免在onDraw
中重复创建。
7. SurfaceView 和普通 View 的区别是什么?适用场景有哪些?
答案:
- 绘制线程不同:
-
普通 View:在主线程(UI 线程)绘制,若绘制任务复杂(如视频解码、游戏渲染),会阻塞主线程,导致 UI 卡顿;
-
SurfaceView:拥有独立的
Surface
(绘图缓冲区),可在子线程绘制,避免阻塞主线程;
- 绘制机制不同:
-
普通 View:绘制内容依赖
Canvas
,与 View 树的绘制流程同步; -
SurfaceView:绘制内容直接渲染到
Surface
,通过SurfaceHolder
管理绘制,支持双缓冲(减少画面闪烁);
- 适用场景:
-
普通 View:适用于简单 UI(如文本、图片);
-
SurfaceView:适用于复杂绘制场景(如视频播放、游戏、实时摄像头预览)。
五、总结
View 绘制机制是安卓 UI 的核心,从Measure
确定宽高,到Layout
确定位置,再到Draw
渲染内容,每个流程都有严格的逻辑和源码规范。日常开发中,需注意避免过度绘制、减少onDraw
中的对象创建,合理使用requestLayout
和invalidate
;面试中,需重点掌握三大流程的原理、MeasureSpec
的模式、getMeasuredWidth
与getWidth
的区别等考点。