2026-02

Android View 的测量与布局流程

理解 measure() → onMeasure() → layout() 的完整链路,以及为什么需要两次测量

MeasureSpec 两次测量 measure() vs onMeasure() 模板方法模式 onDraw

目录导航

一、测量与布局的核心作用

1两大流程总览
流程作用递归入口
测量流程(Measure) 从根 View 递归调用 measure(),确定每个 View 想要的尺寸(宽/高) ViewRootImpl.performMeasure()
布局流程(Layout) 从根 View 递归调用 layout(),把测量好的位置和尺寸传给子 View,子 View 保存 ViewRootImpl.performLayout()
核心目的:确定每个 View 的位置尺寸(相对于父 View),为后续的绘制触摸事件分发提供基础。
2用流程图理解整体链路
ViewRootImpl
performTraversals()
performMeasure()
递归 measure()
performLayout()
递归 layout()
performDraw()
递归 draw()

三大流程按顺序执行,测量必须先于布局(不知道尺寸就无法确定位置),布局必须先于绘制(不知道位置就没法画)。

二、为什么需要分成两个流程?

1核心原因:循环依赖

因为某些情况需要多次测量才能确定最终尺寸。典型场景:

  • 父 View 的尺寸由子 View 决定wrap_content
  • 子 View 的尺寸又依赖父 Viewmatch_parentlayout_weight

这就形成了循环依赖,必须通过多次测量来解耦。

2把测量和布局分开的设计智慧

如果把测量和布局合在一起(一次遍历确定一切),那么遇到上面的循环依赖场景就无法处理——因为你必须在知道父尺寸之前测量子 View,又在知道子尺寸之前确定父尺寸。

分开之后:

  • 第一次测量:用临时约束粗测,收集子 View 的预期尺寸
  • 父 View 确定自己尺寸:基于子 View 的最大/累计值
  • 第二次测量:用父 View 的最终尺寸,给子 View 精确约束重新量

三、实例分析:wrap_content 遇上 match_parent

1场景设置
<LinearLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <View android:layout_width="match_parent" android:layout_height="80dp"/>
    <View android:layout_width="240dp"        android:layout_height="80dp"/>
    <View android:layout_width="160dp"        android:layout_height="80dp"/>

</LinearLayout>

LinearLayout 宽度 = wrap_content(由子 View 决定),但子 View1 宽度 = match_parent(又依赖父 View)。这就是典型的循环依赖。

2第一遍测量

父 View 宽度未知wrap_content尚未计算),以临时约束测量各子 View:

  • 子 View1(match_parent)→ 通常视为 0 或 UNSPECIFIED → 临时宽度 0
  • 子 View2(240dp)→ 测量得到 240dp
  • 子 View3(160dp)→ 测量得到 160dp

LinearLayout 根据子 View 宽度取最大值,确定自己的最终宽度 = 240dp

3第二遍测量

父 View 宽度已确定 = 240dp。再次测量子 View1,此时 match_parent可以生效 → 子 View1 宽度 = 240dp

布局流程使用第二次测量的结果,调用 layout()将位置和尺寸分配给所有子 View。

4极端情况:三个子 View 都是 match_parent

这时 LinearLayout 先让子 View 自由测量,此时最长的子 View 宽度就是 LinearLayout 的宽度,然后用这个宽度对所有子 View 再量一次,这下所有子 View 都 match_parent了——宽度一致。

5垂直 vs 水平排列的差异
场景垂直排列(vertical)水平排列(horizontal)
宽度是 次轴(交叉轴) 主轴
二次测量 触发 forceUniformWidth,所有子 View 宽度统一为最大宽度 不触发二次测量
match_parent 行为 最终都被拉伸为相同宽度 退化为"在剩余空间内 self-wrap",依次挤压
性能提醒:垂直方向会触发二次测量,嵌套层级深时测量时间呈指数级爆炸。应尽量避免在 wrap_content的 LinearLayout 中使用 match_parent,或者用 ConstraintLayout扁平化布局。

四、MeasureSpec:测量的核心协议

1MeasureSpec 的本质
子 View 的 MeasureSpec = 父容器根据自身 MeasureSpec + 子 View 的 LayoutParams 计算得出。
这是 Android 测量流程的起点——父容器通过 MeasureSpec 告诉子 View:"你可以用的空间是多少,以及这个约束是强制的还是建议的。"
2三种模式
模式含义常见场景例子(父容器宽度=1080px)
EXACTLY 精确尺寸,子 View 必须遵守 match_parent/ 固定值 layout_width="100dp"→ (EXACTLY, 100)
AT_MOST 最大不超过此值,子 View 可以更小 wrap_content layout_width="wrap_content"→ (AT_MOST, 1080)
UNSPECIFIED 无约束,父容器不限制 ScrollView 内部、系统特殊场景 ScrollView 内 → (UNSPECIFIED, 0)
3父容器生成子 View MeasureSpec 的关键逻辑
// ViewGroup 中计算子 View MeasureSpec 的核心逻辑(伪代码)
int getChildMeasureSpec(int parentMeasureSpec, int padding, LayoutParams lp) {
    int specMode = MeasureSpec.getMode(parentMeasureSpec);
    int specSize = MeasureSpec.getSize(parentMeasureSpec);
    int size = Math.max(0, specSize - padding); // 剩余可用空间

    if (lp.width == LayoutParams.MATCH_PARENT) {
        return MeasureSpec.makeMeasureSpec(
            specMode == MeasureSpec.EXACTLY ? size : size, specMode);
    } else if (lp.width == LayoutParams.WRAP_CONTENT) {
        return MeasureSpec.makeMeasureSpec(
            specMode == MeasureSpec.EXACTLY || specMode == MeasureSpec.AT_MOST ? size : 0,
            MeasureSpec.AT_MOST);
    } else {
        return MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY);
    }
}

五、measure() vs onMeasure() — 模板方法模式

1为什么要分两个方法?

这是 Android 设计中的模板方法模式的经典应用。核心目的:把"测量流程的框架""具体的测量逻辑"分离。

方法角色能否重写职责
measure() 流程框架 final,禁止重写 缓存检查 → 清除标志 → 调用 onMeasure() → 校验 → 保存
onMeasure() 具体测量 protected,子类重写 根据 MeasureSpec 计算尺寸 → setMeasuredDimension()
如果允许子类重写 measure(),很容易破坏整个测量契约(比如忘记调用 setMeasuredDimension,或者不调用 onMeasure),导致 View 系统行为异常。所以 Android 把它设为 final,子类只能重写 onMeasure()
2measure() 源码简化版
// View.java — measure() 是 final,框架保证流程不被破坏
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    // 1. 检查缓存:布局参数没变、标志位没要求强制重测、规格相同 → 跳过
    boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
    if (!forceLayout && widthMeasureSpec == mOldWidthMeasureSpec
            && heightMeasureSpec == mOldHeightMeasureSpec) {
        return;  // 尺寸没变,直接返回,避免重复计算
    }

    // 2. 清除测量标志,准备重新测量
    mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

    // 3. 调用 onMeasure 让子类实际计算尺寸(这是唯一允许子类介入的点)
    onMeasure(widthMeasureSpec, heightMeasureSpec);

    // 4. 严格校验:onMeasure 中必须调用 setMeasuredDimension,否则抛异常
    if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
        throw new IllegalStateException("onMeasure() did not set the measured dimension");
    }

    // 5. 保存此次规格,供下次缓存判断
    mOldWidthMeasureSpec = widthMeasureSpec;
    mOldHeightMeasureSpec = heightMeasureSpec;
}

// onMeasure 默认实现:根据背景最小值 + MeasureSpec 默认规则
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(
        getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
        getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)
    );
}
设计精髓:measure()是不变的部分(缓存、标志位、异常检查、调用钩子)→ final,禁止修改。onMeasure()是可变的部分(不同 View 的尺寸计算逻辑不同)→ protected,开放重写。开发者只需关心如何计算尺寸,不用重复编写缓存、校验等样板代码。

六、单个 View 的测量全链路

1五个步骤(从 XML 到最终尺寸)
1. 开发者在 XML 中
写 layout_xxx 要求
2. 父 View 在 onMeasure() 中
生成子 View 的 MeasureSpec
3. 子 View 在 onMeasure() 中
算出自己的期望尺寸
4. 父 View 得出子 View
的实际尺寸和位置
5. 子 View 在 layout() 中
保存最终尺寸和位置

逐步骤细致说明:

  1. 运行前:开发者在 XML 中写入对 View 的布局要求(layout_widthlayout_height等)
  2. 父 View 生成约束:在自己的 onMeasure()中,根据子 View 的 LayoutParams 和自己的可用空间,得出对子 View 的具体尺寸要求(MeasureSpec)
  3. 子 View 计算期望尺寸:在自己的 onMeasure()中,根据父 View 的要求以及自己的特性算出期望尺寸。如果是 ViewGroup,还会在这里调用每个子 View 的 measure()进行递归测量
  4. 父 View 定案:在子 View 计算出期望尺寸后,父 View 得出子 View 的实际尺寸和位置
  5. 子 View 保存结果:在自己的 layout()方法中,将父 View 传进来的实际尺寸和位置保存
2关键规则:父 View 永远调用子 View 的 measure()
父 View 调用的永远是子 View 的 measure()方法,由 measure()负责调用 onMeasure()完成实际测量,并保证流程规范(缓存、校验、异常处理)。

这保证了:无论哪种 ViewGroup 的实现,子 View 的测量入口始终是统一的 measure(),测量契约不会被破坏。

七、正确实现 onMeasure()(支持 wrap_content + padding)

错误示例(wrap_content 失效)
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    // 错误:直接返回父容器给的尺寸(AT_MOST 模式下等于父容器宽度)
    val width = MeasureSpec.getSize(widthMeasureSpec)
    val height = MeasureSpec.getSize(heightMeasureSpec)
    setMeasuredDimension(width, height)
}
正确模板(使用 resolveSize)
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    // 1. 计算内容区域所需最小尺寸(不含 padding)
    val contentWidth = 200  // 你的内容宽度(如文字/圆的直径)
    val contentHeight = 200

    // 2. 加上 padding(关键!否则内容会贴边)
    val totalWidth = contentWidth + paddingLeft + paddingRight
    val totalHeight = contentHeight + paddingTop + paddingBottom

    // 3. 按父容器约束决定最终尺寸(必须!)
    val width = resolveSize(totalWidth, widthMeasureSpec)
    val height = resolveSize(totalHeight, heightMeasureSpec)

    // 4. 设置测量结果
    setMeasuredDimension(width, height)
}
为什么用 resolveSize?
它内部处理了三种模式:
  • EXACTLY→ 返回 specSize(强制遵守)
  • AT_MOST→ 返回 min(specSize, desiredSize)(不能超出)
  • UNSPECIFIED→ 返回 desiredSize(自由决定)

八、绘制时正确处理 padding

错误写法(内容贴边)
override fun onDraw(canvas: Canvas) {
    // 错误:直接从 (0,0) 开始画,忽略 padding
    canvas.drawCircle(0f, 0f, 50f, paint)
}
正确写法(在 padding 区域内绘制)
override fun onDraw(canvas: Canvas) {
    // 计算可绘制区域(避开 padding)
    val drawLeft = paddingLeft.toFloat()
    val drawTop = paddingTop.toFloat()
    val drawRight = (width - paddingRight).toFloat()
    val drawBottom = (height - paddingBottom).toFloat()

    // 在可绘制区域内画圆
    val centerX = drawLeft + (drawRight - drawLeft) / 2f
    val centerY = drawTop + (drawBottom - drawTop) / 2f
    val radius = min(drawRight - drawLeft, drawBottom - drawTop) / 2f
    canvas.drawCircle(centerX, centerY, radius, paint)
}
重要原则:padding 是 View 自身属性,必须在 onMeasureonDraw手动处理

九、margin 由父容器处理,View 无需关心

1属性所属对比
属性所属View 内部是否需要处理举例
padding View 自身 必须处理 android:padding="16dp"
margin 父容器约束 完全不用管 android:layout_margin="8dp"

父容器在测量子 View 时,会自动扣除 margin(通过 getChildMeasureSpec),子 View 的 measuredWidth不包含 margin。

十、核心思想总结

四句话记住 View 测量与布局

  1. MeasureSpec 是父 View 给子 View 的"空间合同"——规定了你可以用多大空间(size)以及这个约束是强制的还是建议的(mode)
  2. measure() 是 final 框架,onMeasure() 是开放策略——模板方法模式的经典实践,保证测量契约不被破坏
  3. 两次测量的本质是解耦循环依赖——第一遍粗测收集信息,父 View 定案,第二遍精确测量
  4. padding 自己管,margin 父容器管——onMeasure 和 onDraw 中必须手动处理 padding,margin 由父容器自动扣除

完整调用链

ViewRootImpl
performTraversals()
performMeasure()
→ measure() [final]
→ onMeasure() [子类重写]
→ setMeasuredDimension()
performLayout()
→ layout()
→ onLayout() [ViewGroup 重写]
子 View 保存位置
performDraw()
→ draw()
→ onDraw() + dispatchDraw()

正确做法

  • onMeasure 中用 resolveSize() 处理三种模式
  • onMeasure 中加上 paddingLeft + paddingRight
  • onDraw 中用 paddingLeft / paddingTop 偏移绘制区域
  • View 中用 post / postDelayed 替代 Handler
  • onDetachedFromWindow 中清理资源

常见错误

  • 直接 getSize() 导致 wrap_content 失效
  • onDraw 忽略 padding 导致内容贴边
  • 在 View 中创建 Handler(内存泄漏)
  • 在 wrap_content 的 LinearLayout 中大量使用 match_parent(二次测量开销)