Android View 绘制流程

16 阅读5分钟

Android View 绘制流程:onMeasure、onLayout、onDraw

源码参考:AOSP frameworks/base (View.java, ViewRootImpl.java)

一、整体流程与顺序

ViewRootImpl.performTraversals()
    │
    ├── 1. performMeasure()   ──→ measure() ──→ onMeasure()
    │
    ├── 2. performLayout()   ──→ layout()  ──→ onLayout()
    │
    └── 3. performDraw()     ──→ draw()    ──→ onDraw()

调用顺序:Measure → Layout → Draw,三者依次执行,不可跳过。

触发时机requestLayout()scheduleTraversals()(与 Vsync 同步)→ doTraversal()performTraversals()


二、Measure 流程

2.1 入口:View.measure()

// View.java
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    // 步骤1:光学边距调整(若启用)
    boolean optical = isLayoutModeOptical(this);
    if (optical != isLayoutModeOptical(mParent)) {
        Insets insets = getOpticalInsets();
        widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, ...);
        heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, ...);
    }

    // 步骤2:判断是否需要重新测量
    final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
    final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
            || heightMeasureSpec != mOldHeightMeasureSpec;
    final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
            && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
    final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
            && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
    final boolean needsLayout = specChanged && (!isSpecExactly || !matchesSpecSize);

    if (forceLayout || needsLayout) {
        mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;  // 清除已测量标志

        // 步骤3:检查测量缓存,未命中则调用 onMeasure
        if (cacheIndex < 0) {
            onMeasure(widthMeasureSpec, heightMeasureSpec);
        } else {
            setMeasuredDimensionRaw(...);  // 使用缓存
        }

        // 步骤4:校验 onMeasure 是否调用了 setMeasuredDimension
        if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
            throw new IllegalStateException("onMeasure() did not set the measured dimension");
        }

        mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;  // 标记需要 layout
    }

    mOldWidthMeasureSpec = widthMeasureSpec;
    mOldHeightMeasureSpec = heightMeasureSpec;
}

2.2 核心:onMeasure()

// View.java - 默认实现
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(
        getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
        getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)
    );
}

2.3 Measure 阶段需完成的步骤

步骤逻辑说明
1接收 MeasureSpec父 View 传入宽高约束(mode + size)
2计算自身尺寸根据 EXACTLY / AT_MOST / UNSPECIFIED 决定宽高
3测量子 View(ViewGroup)遍历子 View,调用 child.measure(childWidthSpec, childHeightSpec)
4调用 setMeasuredDimension()必须调用,否则抛 IllegalStateException
5存储结果通过 getMeasuredWidth() / getMeasuredHeight() 获取

2.4 MeasureSpec 说明

Mode含义
EXACTLY精确尺寸,如 match_parent 或具体 dp
AT_MOST最大尺寸,如 wrap_content,子 View 不超过此值
UNSPECIFIED无限制,如 ScrollView 对子 View 的高度

三、Layout 流程

3.1 入口:View.layout()

// View.java
public void layout(int l, int t, int r, int b) {
    // 步骤1:若标记了 MEASURE_NEEDED_BEFORE_LAYOUT,先执行 measure
    if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
        onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
        mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    }

    int oldL = mLeft, oldT = mTop, oldB = mBottom, oldR = mRight;

    // 步骤2:设置自身位置(setFrame)
    boolean changed = isLayoutModeOptical(mParent) ?
        setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

    // 步骤3:若位置变化或需要 layout,调用 onLayout
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        onLayout(changed, l, t, r, b);
        mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
    }

    // 步骤4:通知 OnLayoutChangeListener
    if (li != null && li.mOnLayoutChangeListeners != null) {
        for (OnLayoutChangeListener listener : listenersCopy) {
            listener.onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
        }
    }

    mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}

3.2 核心:onLayout()

// View.java - 默认空实现
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

// ViewGroup 子类(如 FrameLayout)需重写,对每个子 View 调用:
// child.layout(left, top, right, bottom);

3.3 Layout 阶段需完成的步骤

步骤逻辑说明
1接收位置参数父 View 传入 l, t, r, b(相对父的坐标)
2setFrame()设置 mLeft、mTop、mRight、mBottom
3确定子 View 位置(ViewGroup)根据 measure 结果,计算每个子 View 的 l,t,r,b
4调用 child.layout()对每个子 View 调用 layout(l, t, r, b)
5通知监听器触发 OnLayoutChangeListener

四、Draw 流程

4.1 入口:View.draw()

// View.java - draw() 注释中的 7 步
public void draw(Canvas canvas) {
    final int privateFlags = mPrivateFlags;
    mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

    /*
     * 1. Draw the background
     * 2. If necessary, save the canvas' layers to prepare for fading
     * 3. Draw view's content
     * 4. Draw children
     * 5. If necessary, draw the fading edges and restore layers
     * 6. Draw decorations (scrollbars for instance)
     * 7. If necessary, draw the default focus highlight
     */

    // Step 1, draw the background
    drawBackground(canvas);

    // Step 3, draw the content
    onDraw(canvas);

    // Step 4, draw the children
    dispatchDraw(canvas);

    // Step 6, draw decorations (foreground, scrollbars)
    onDrawForeground(canvas);

    // Step 7, draw the default focus highlight
    drawDefaultFocusHighlight(canvas);
}

4.2 核心:onDraw()

// View.java - 默认空实现
protected void onDraw(Canvas canvas) {
}

// 自定义 View 重写此方法绘制内容

4.3 dispatchDraw()(ViewGroup 绘制子 View)

// ViewGroup 重写 dispatchDraw,遍历子 View 并调用:
protected void dispatchDraw(Canvas canvas) {
    for (int i = 0; i < childrenCount; i++) {
        drawChild(canvas, child, drawingTime);
    }
}

4.4 Draw 阶段需完成的步骤

步骤方法说明
1drawBackground()绘制背景
2(可选)saveLayer为 fading 等效果保存图层
3onDraw()绘制 View 自身内容
4dispatchDraw()绘制子 View(ViewGroup 遍历 drawChild)
5(可选)fading edges绘制边缘渐变
6onDrawForeground()绘制前景、滚动条等装饰
7drawDefaultFocusHighlight()绘制焦点高亮

4.5 绘制顺序

  • 父 View 先于子 View 绘制(父在下层,子在上层)
  • 同层级按添加顺序绘制(先添加的在下面)

五、流程总览

performTraversals()
    │
    ├── performMeasure()
    │       └── mView.measure(widthSpec, heightSpec)
    │               └── onMeasure() ──→ setMeasuredDimension()
    │
    ├── performLayout()
    │       └── mView.layout(0, 0, width, height)
    │               └── setFrame() ──→ onLayout()
    │
    └── performDraw()
            └── mView.draw(canvas)
                    ├── drawBackground()
                    ├── onDraw()
                    ├── dispatchDraw() ──→ 递归子 View.draw()
                    └── onDrawForeground()

六、各阶段职责小结

阶段职责必须完成
Measure确定 View 的宽高调用 setMeasuredDimension()
Layout确定 View 的位置(l,t,r,b)ViewGroup 需对子 View 调用 layout()
Draw将 View 绘制到 Canvas重写 onDraw() 绘制内容,ViewGroup 通过 dispatchDraw() 绘制子 View

七、Activity 生命周期与 View 宽高获取时机

7.1 onResume 中能否拿到 View 宽高?

结论:通常拿不到,getHeight() / getWidth() 多为 0。

原因onResume()performTraversals() 之前执行,measure/layout 尚未完成。

handleResumeActivity()
    │
    ├── performResumeActivity()  ──→ Activity.onResume()
    │       └── 【此时 onResume 执行,View 尚未 measure/layout】
    │
    ├── wm.addView(decor, layoutParams)
    │
    ├── ViewRootImpl.setView(decor)  ──→ requestLayout()  ──→ scheduleTraversals()
    │
    └── 下一帧 Choreographer 回调
            └── doTraversal()  ──→ performTraversals()
                    ├── performMeasure()
                    ├── performLayout()   ← 此时才设置 mLeft、mTop、mRight、mBottom
                    └── performDraw()

getHeight() 返回 0 的原因

// View.java
public final int getHeight() {
    return mBottom - mTop;  // layout 之后才有值
}

mLeftmTopmRightmBottomView.layout()setFrame() 中才被赋值,而 layout 在 onResume 返回之后执行。

7.2 View.post() 可以拿到宽高

原理View.post() 将 Runnable 投递到主线程消息队列末尾,会在当前帧的 measure/layout 之后执行。

// View.java
public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        return attachInfo.mHandler.post(action);  // 主线程 Handler
    }
    getRunQueue().post(action);  // 未 attach 时先入队,attach 后再 post
    return true;
}

执行顺序

主线程消息队列
    │
    ├── Choreographer 回调 ──→ doTraversal() ──→ performMeasure() ──→ performLayout() ──→ performDraw()
    │       └── layout 完成,mLeft/mTop/mRight/mBottom 已赋值
    │
    └── View.post(runnable) 的 Runnable  ← 此时执行,可拿到宽高

使用示例

view.post {
    val width = view.width   // 可拿到
    val height = view.height // 可拿到
}

7.3 其他获取宽高的方式

方式说明
ViewTreeObserver.OnGlobalLayoutListeneronGlobalLayout() 在 measure/layout 完成后触发,最稳妥
View.post()投递到队列末尾,通常 layout 已完成,简单常用
view.doOnLayout {}AndroidX 扩展,等价于 OnGlobalLayoutListener

7.4 各阶段能否获取宽高

阶段是否可拿到高度
onCreate❌ 不可
onStart❌ 不可
onResume❌ 不可(measure/layout 尚未执行)
onGlobalLayout 回调✅ 可
View.post 执行时✅ 可
onWindowFocusChanged(true)⚠️ 通常可(首帧已绘制)

八、自定义 View 注意点

  1. onMeasure:必须调用 setMeasuredDimension(),否则抛异常。
  2. onLayout:叶子 View 可不重写;ViewGroup 必须重写并调用每个 child.layout()
  3. onDraw:在 draw() 的步骤 3 中调用,只负责自身内容;子 View 由 dispatchDraw() 处理。
  4. requestLayout():触发重新 measure + layout,不一定会 draw。
  5. invalidate():触发重新 draw,不触发 measure/layout。