安卓 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的区别等考点。