Android 的 UI 绘制流程,通常说的是 View 绘制流程,核心可以概括为三大步骤:
measure → layout → draw
测量 布局 绘制
也就是:
View 多大? View 放哪? View 怎么画?
一、View 绘制流程总览
Android 中一个界面的根 View 通常是:
Activity
↓
Window
↓
DecorView
↓
ContentView
↓
ViewGroup / View
当 Activity 启动后,系统会把布局文件加载成 View 树,然后由 ViewRootImpl 发起绘制流程。
整体流程大致是:
Activity.setContentView()
↓
生成 DecorView / ContentView
↓
WindowManager.addView()
↓
创建 ViewRootImpl
↓
ViewRootImpl.performTraversals()
↓
performMeasure()
↓
performLayout()
↓
performDraw()
其中最核心的方法是:
ViewRootImpl.performTraversals()
它内部会依次执行:
performMeasure();
performLayout();
performDraw();
对应到 View 层就是:
measure()
layout()
draw()
二、View 绘制三大流程
1. measure:测量阶段
作用:
确定 View 的宽高
入口方法:
measure(int widthMeasureSpec, int heightMeasureSpec)
内部会调用:
onMeasure(int widthMeasureSpec, int heightMeasureSpec)
一般自定义 View 时,重写的是:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
measure 阶段做什么?
父 View 会根据自己的大小、子 View 的 LayoutParams,生成子 View 的 MeasureSpec,然后让子 View 测量自己。
例如:
父 ViewGroup 测量自己
↓
遍历子 View
↓
调用 child.measure()
↓
子 View 执行 onMeasure()
↓
确定 measuredWidth / measuredHeight
测量完成后,View 的测量宽高可以通过下面方法获取:
getMeasuredWidth()
getMeasuredHeight()
三、MeasureSpec 是什么?
MeasureSpec 是 View 测量过程中非常重要的概念。
它由两部分组成:
测量模式 mode + 测量大小 size
可以理解成:
父容器对子 View 的尺寸要求
MeasureSpec 的三种模式
1. EXACTLY
精确模式。
表示父容器已经确定了子 View 的大小。
常见于:
layout_width="100dp"
layout_width="match_parent"
例如:
你就必须是 100dp
2. AT_MOST
最大模式。
表示子 View 最大不能超过某个值。
常见于:
layout_width="wrap_content"
例如:
你想多大都行,但不能超过父容器给你的最大范围
3. UNSPECIFIED
未指定模式。
父容器不对子 View 做限制。
较少见,常见于:
ScrollView 内部测量
四、layout:布局阶段
作用:
确定 View 的位置
入口方法:
layout(int l, int t, int r, int b)
内部会调用:
onLayout(boolean changed, int l, int t, int r, int b)
layout 阶段做什么?
measure 阶段只是确定了 View 的宽高,但还不知道它放在哪。
layout 阶段会确定 View 在父容器中的四个坐标:
left
top
right
bottom
也就是:
getLeft()
getTop()
getRight()
getBottom()
View 和 ViewGroup 的区别
普通 View
普通 View 自己没有子 View,所以一般不需要重写 onLayout()。
ViewGroup
ViewGroup 需要决定每个子 View 放在哪里,所以自定义 ViewGroup 通常要重写:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 摆放子 View
}
例如伪代码:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childLeft = 0;
int childTop = 0;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
child.layout(
childLeft,
childTop,
childLeft + childWidth,
childTop + childHeight
);
childTop += childHeight;
}
}
五、draw:绘制阶段
作用:
把 View 画到屏幕上
入口方法:
draw(Canvas canvas)
内部会调用:
onDraw(Canvas canvas)
自定义 View 时,通常重写:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 自己的绘制逻辑
}
六、draw 的完整绘制顺序
View 的 draw() 方法内部并不是只调用 onDraw(),它有一套固定顺序。
大致流程:
1. 绘制背景 background
2. 绘制自身内容 onDraw()
3. 绘制子 View dispatchDraw()
4. 绘制装饰内容,比如滚动条、前景等 onDrawForeground()
可以理解为:
draw(Canvas canvas) {
drawBackground(canvas); // 绘制背景
onDraw(canvas); // 绘制自己
dispatchDraw(canvas); // 绘制子 View
onDrawForeground(canvas); // 绘制前景、滚动条等
}
对普通 View 来说
一般关注:
onDraw(Canvas canvas)
例如:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.RED);
canvas.drawCircle(100, 100, 50, paint);
}
对 ViewGroup 来说
ViewGroup 默认通常不会执行自己的 onDraw(),因为它默认设置了:
setWillNotDraw(true)
如果自定义 ViewGroup 想绘制自己的内容,需要调用:
setWillNotDraw(false);
例如:
public class MyLayout extends ViewGroup {
public MyLayout(Context context) {
super(context);
setWillNotDraw(false);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 绘制 ViewGroup 自身内容
}
}
七、完整调用链
以一个 Activity 页面绘制为例:
Activity.setContentView()
↓
PhoneWindow.setContentView()
↓
DecorView 添加布局
↓
WindowManager.addView()
↓
ViewRootImpl.setView()
↓
requestLayout()
↓
scheduleTraversals()
↓
Choreographer.postCallback()
↓
ViewRootImpl.performTraversals()
↓
performMeasure()
↓
DecorView.measure()
↓
ViewGroup.onMeasure()
↓
child.measure()
↓
child.onMeasure()
↓
performLayout()
↓
DecorView.layout()
↓
ViewGroup.onLayout()
↓
child.layout()
↓
performDraw()
↓
DecorView.draw()
↓
ViewGroup.draw()
↓
View.draw()
↓
onDraw()
核心就是:
ViewRootImpl.performTraversals()
→ performMeasure()
→ performLayout()
→ performDraw()
八、measure、layout、draw 分别对应什么方法?
| 阶段 | 入口方法 | 可重写方法 | 作用 |
|---|---|---|---|
| 测量 | measure() |
onMeasure() |
确定宽高 |
| 布局 | layout() |
onLayout() |
确定位置 |
| 绘制 | draw() |
onDraw() |
绘制内容 |
九、几个常见方法的区别
1. getMeasuredWidth() 和 getWidth()
getMeasuredWidth()
来自测量阶段:
getMeasuredWidth()
表示:
View 测量出来的宽度
在 onMeasure() 后可以获取。
getWidth()
来自布局阶段:
getWidth()
本质是:
getRight() - getLeft()
表示:
View 最终布局后的实际宽度
通常在 onLayout() 之后才准确。
区别
getMeasuredWidth():测量宽度
getWidth():最终显示宽度
大多数情况下它们相等,但不一定永远相等。
十、invalidate 和 requestLayout 的区别
这是面试高频。
1. invalidate()
作用:
请求重新绘制
一般只会触发:
draw
不会重新执行:
measure
layout
适合场景:
View 的内容变了,但大小和位置没变
例如:
paint.setColor(Color.RED);
invalidate();
生命周期:
draw
2. requestLayout()
作用:
请求重新布局
一般会触发:
measure
layout
draw
适合场景:
View 的大小或位置可能发生变化
例如:
LayoutParams lp = getLayoutParams();
lp.width = 300;
setLayoutParams(lp);
requestLayout();
生命周期:
measure → layout → draw
对比
| 方法 | 作用 | 触发流程 | 使用场景 |
|---|---|---|---|
invalidate() |
重绘 | draw |
内容变化 |
requestLayout() |
重新布局 | measure → layout → draw |
尺寸、位置变化 |
postInvalidate() |
子线程请求重绘 | draw |
子线程中刷新 UI |
十一、自定义 View 中常用重写方法
1. 自定义普通 View
一般重写:
onMeasure()
onDraw()
例如:
public class CircleView extends View {
private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
public CircleView(Context context) {
super(context);
paint.setColor(Color.RED);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int defaultSize = 200;
int width = resolveSize(defaultSize, widthMeasureSpec);
int height = resolveSize(defaultSize, heightMeasureSpec);
setMeasuredDimension(width, height);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float radius = Math.min(getWidth(), getHeight()) / 2f;
canvas.drawCircle(getWidth() / 2f, getHeight() / 2f, radius, paint);
}
}
2. 自定义 ViewGroup
一般重写:
onMeasure()
onLayout()
如果 ViewGroup 自己也要画东西,再重写:
onDraw()
例如:
public class MyViewGroup extends ViewGroup {
public MyViewGroup(Context context) {
super(context);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int totalHeight = 0;
int maxWidth = 0;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
totalHeight += child.getMeasuredHeight();
maxWidth = Math.max(maxWidth, child.getMeasuredWidth());
}
setMeasuredDimension(
resolveSize(maxWidth, widthMeasureSpec),
resolveSize(totalHeight, heightMeasureSpec)
);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int top = 0;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
int height = child.getMeasuredHeight();
child.layout(
0,
top,
child.getMeasuredWidth(),
top + height
);
top += height;
}
}
}
十二、绘制流程触发时机
常见触发场景:
1. Activity 首次显示
measure → layout → draw
2. 调用 requestLayout()
measure → layout → draw
3. 调用 invalidate()
draw
4. View 尺寸变化
例如:
横竖屏切换
键盘弹出
窗口大小变化
可能触发:
measure → layout → draw
十三、Choreographer 和 VSync
Android UI 刷新不是随便立即执行的,而是依赖屏幕刷新信号。
大致流程:
requestLayout()/invalidate()
↓
ViewRootImpl.scheduleTraversals()
↓
Choreographer.postCallback()
↓
等待下一次 VSync 信号
↓
doFrame()
↓
performTraversals()
也就是说:
UI 刷新通常会被安排到下一帧执行
屏幕一般是 60Hz 时:
一帧大约 16.6ms
如果一帧内 UI 绘制、布局、动画、输入处理等耗时超过 16.6ms,就容易出现:
掉帧 / 卡顿
十四、面试版回答
如果面试官问:
Android View 的绘制流程是什么?
可以这样回答:
Android View 的绘制流程主要分为 measure、layout、draw 三个阶段。
整个流程由 ViewRootImpl 发起,核心方法是 performTraversals()。它内部会依次调用 performMeasure、performLayout 和 performDraw。
measure 阶段用于测量 View 的宽高,对应 measure() 和 onMeasure();父 View 会根据自己的 MeasureSpec 和子 View 的 LayoutParams 生成子 View 的 MeasureSpec。
layout 阶段用于确定 View 的位置,对应 layout() 和 onLayout(),最终确定 left、top、right、bottom。
draw 阶段用于真正绘制内容,对应 draw() 和 onDraw()。draw 的顺序通常是先绘制背景,再绘制自身内容,然后绘制子 View,最后绘制前景、滚动条等。
如果调用 invalidate,一般只会触发重新 draw;如果调用 requestLayout,一般会触发重新 measure、layout 和 draw。
十五、核心总结
View 绘制三大流程:
measure → layout → draw
分别解决三个问题:
measure:View 多大?
layout :View 放哪?
draw :View 怎么画?
核心入口:
ViewRootImpl.performTraversals()
核心方法:
performMeasure()
performLayout()
performDraw()
常见刷新方式:
invalidate() → 重新 draw
requestLayout() → 重新 measure、layout、draw
一句话总结:
Android 的 UI 绘制是由 ViewRootImpl 发起的,从根 View DecorView 开始遍历整棵 View 树,依次完成测量、布局和绘制,最终把界面内容显示到屏幕上。