Android 的 UI 绘制流程

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 树,依次完成测量、布局和绘制,最终把界面内容显示到屏幕上。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇