Android View 绘制流程

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

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

一、整体流程与顺序

scss 复制代码
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()

java 复制代码
// 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()

java 复制代码
// 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()

java 复制代码
// 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()

java 复制代码
// 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(相对父的坐标)
2 setFrame() 设置 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()

java 复制代码
// 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()

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

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

4.3 dispatchDraw()(ViewGroup 绘制子 View)

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

4.4 Draw 阶段需完成的步骤

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

4.5 绘制顺序

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

五、流程总览

scss 复制代码
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 尚未完成。

scss 复制代码
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 的原因

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

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

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

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

java 复制代码
// 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;
}

执行顺序

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

使用示例

kotlin 复制代码
view.post {
    val width = view.width   // 可拿到
    val height = view.height // 可拿到
}

7.3 其他获取宽高的方式

方式 说明
ViewTreeObserver.OnGlobalLayoutListener onGlobalLayout() 在 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。
相关推荐
南城书生2 小时前
# Android 常见内存泄漏
前端
wefly20172 小时前
M3U8 播放调试天花板!m3u8live.cn纯网页无广告,音视频开发效率直接拉满
java·前端·javascript·python·音视频
陈林梓2 小时前
异步组件、动态插槽
前端
喝咖啡的女孩2 小时前
前端巨型列表渲染
前端
兆子龙2 小时前
antd 组件也做了同款效果!深入源码看设计模式在前端组件库的应用
java·前端·架构
前端Hardy2 小时前
Flutter vs React Native vs HarmonyOS:谁更适合下一代跨端?2026 年技术选型终极指南
前端·flutter·react native
前端Hardy2 小时前
Vite 8 来了:彻底抛弃 Rollup 和 esbuild!Rust 重写后,快到 Webpack 连尾灯都看不见
前端·面试·vite
兆子龙2 小时前
lodash 到 lodash-es 多的不仅仅是后缀!深入源码看 ES Module 带来的性能与体积优化
java·前端·架构
heyCHEEMS2 小时前
用 分段渲染 解决小程序长列表卡顿问题
前端·微信小程序