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 之后才有值
}
mLeft、mTop、mRight、mBottom 在 View.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 注意点
- onMeasure :必须调用
setMeasuredDimension(),否则抛异常。 - onLayout :叶子 View 可不重写;ViewGroup 必须重写并调用每个
child.layout()。 - onDraw :在
draw()的步骤 3 中调用,只负责自身内容;子 View 由dispatchDraw()处理。 - requestLayout():触发重新 measure + layout,不一定会 draw。
- invalidate():触发重新 draw,不触发 measure/layout。