☰
Android measure 过程详解:结合 4.0.4 源码看 View、MeasureSpec 与 onMeasure 的协作
2026/10/4 9:09:11 网站建设 项目流程

1. 从一次「自定义 View 尺寸不对」说起:measure 链路到底怎么走

如果你写过自定义 View,大概率遇到过这种场景:XML 里明明写了wrap_content,结果控件要么撑满父容器,要么直接变成 0 宽 0 高。很多人第一反应是去改onDraw,其实问题根本不在绘制,而在measure 过程——也就是 Android 里 View、MeasureSpec 与 onMeasure 三者协作决定「这个 View 到底多大」的那条链路。

这篇文章聚焦 Android 4.0.4 源码,把 measure 的完整链路从ViewRootImpl触发一路拆到View.measure、MeasureSpec生成、onMeasure回调,重点讲清楚父容器的约束是怎么一层层传到子 View 的。看完你应该能回答三个问题:为什么measure是final的、getChildMeasureSpec那张表怎么查、自定义 View 的onMeasure模板该怎么写才不踩坑。

适合谁看:写过自定义 View 但wrap_content总出问题的同学;想搞懂MeasureSpec三种模式区别的同学;准备面试被问「measure 流程」想讲清楚源码细节的同学。全程结合 4.0.4 的真实函数签名和代码,你可以直接复制模板到项目里跑。

先说结论:measure 的本质就是给每个 View 的mMeasuredWidth/mMeasuredHeight赋值,而这两个值由「父容器给的 MeasureSpec + 自己的 LayoutParams」共同决定。理解了这个「约束传递」模型,后面所有细节都是它的展开。

2. ViewRootImpl 触发与 View.measure 源码:measure 为什么是 final

measure 的起点不在 View 自己,而在ViewRootImpl.performTraversals()。当界面需要刷新时,ViewRootImpl 会依次判断是否需要 measure、layout、draw,其中 measure 是前提——因为 layout 要用到测量结果,draw 又要用到 layout 确定的位置。所以整条链路是:ViewRootImpl.performTraversals()→performMeasure()→mView.measure(childWidthMeasureSpec, childHeightMeasureSpec),从根 View(DecorView)开始向下递归。

进入View.measure后,4.0.4 的源码是这样的:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) { if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT || widthMeasureSpec != mOldWidthMeasureSpec || heightMeasureSpec != mOldHeightMeasureSpec) { mPrivateFlags &= ~MEASURED_DIMENSION_SET; if (ViewDebug.TRACE_HIERARCHY) { ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_MEASURE); } onMeasure(widthMeasureSpec, heightMeasureSpec); if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) { throw new IllegalStateException("onMeasure() did not set the" + " measured dimension by calling" + " setMeasuredDimension()"); } mPrivateFlags |= LAYOUT_REQUIRED; } mOldWidthMeasureSpec = widthMeasureSpec; mOldHeightMeasureSpec = heightMeasureSpec; }

这里有几个关键点值得逐条拆开。

第一,measure被声明为final,意味着子类不能重写 measure 本身。Google 这么设计是为了保证测量流程的固定性:先清标志位、再回调onMeasure、最后校验标志位。真正留给开发者扩展的入口只有onMeasure。所以你在自定义 View 里永远不该去 overridemeasure,那是死路。

第二,注意那个if判断。只有当FORCE_LAYOUT被置位,或者新的 MeasureSpec 和上一次(mOldWidthMeasureSpec/mOldHeightMeasureSpec)不一样时,才会真正走onMeasure。这就是为什么同一个尺寸下重复请求布局,onMeasure 不一定被调用——系统做了缓存。这一点在验证测量次数时特别重要,后面第 4 节会用日志证明。

第三,onMeasure执行完后会检查MEASURED_DIMENSION_SET标志。如果你重写onMeasure却忘了调用setMeasuredDimension,这里会直接抛IllegalStateException,报错信息就是那句经典的「onMeasure() did not set the measured dimension by calling setMeasuredDimension()」。这是新手最常见的崩溃之一。

再看默认的onMeasure实现:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }

它只做了一件事:调用setMeasuredDimension。而这个函数才是真正给mMeasuredWidth/mMeasuredHeight赋值的地方:

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { mMeasuredWidth = measuredWidth; mMeasuredHeight = measuredHeight; mPrivateFlags |= MEASURED_DIMENSION_SET; }

所以 measure 的最终目的,就是让每个 View 的这两个成员变量有值。对于非 ViewGroup 的普通 View,走默认实现就够了;对于 ViewGroup,通常要重写onMeasure去测量所有子 View,但重写时千万别忘了最后调用setMeasuredDimension设置自身大小,否则一样会抛异常。

3. MeasureSpec 与 getChildMeasureSpec:父约束如何传到子 View

measure(int widthMeasureSpec, int heightMeasureSpec)的两个参数,就是父视图传给子视图的「测量规格」。它是一个 int,高 32 位存specMode,低 16 位存specSize。三种模式必须记牢:

  • MeasureSpec.UNSPECIFIED:父容器不给任何限制,子 View 想要多大就多大,一般出现在 ScrollView、ListView 这种可滚动容器里。
  • MeasureSpec.EXACTLY:父容器已经定死了大小,子 View 必须用specSize,对应match_parent或具体数值。
  • MeasureSpec.AT_MOST:子 View 最大不能超过specSize,对应wrap_content。

ViewGroup 提供了三个测量子 View 的方法:measureChildren、measureChild、measureChildWithMargins。前两个的区别是是否考虑 padding,而measureChildWithMargins额外把 margin 也算进去。核心逻辑在measureChildWithMargins:

protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }

它做的事情就是:拿父容器的 MeasureSpec,减去自己占用的 padding、margin、已用空间,再结合子 View 的LayoutParams,算出子 View 该用的 MeasureSpec,然后调用child.measure()。真正的换算逻辑在getChildMeasureSpec:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) { int specMode = MeasureSpec.getMode(spec); int specSize = MeasureSpec.getSize(spec); int size = Math.max(0, specSize - padding); int resultSize = 0; int resultMode = 0; switch (specMode) { case MeasureSpec.EXACTLY: if (childDimension >= 0) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { resultSize = size; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) { resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; case MeasureSpec.AT_MOST: if (childDimension >= 0) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { resultSize = size; resultMode = MeasureSpec.AT_MOST; } else if (childDimension == LayoutParams.WRAP_CONTENT) { resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; case MeasureSpec.UNSPECIFIED: if (childDimension >= 0) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { resultSize = 0; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { resultSize = 0; resultMode = MeasureSpec.UNSPECIFIED; } break; } return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }

这张表建议你背下来,它是 measure 里最容易出错的点。用表格对照更直观:

父 specMode子 LayoutParams结果 mode结果 size
EXACTLY具体值 dpEXACTLY具体值
EXACTLYmatch_parentEXACTLY父剩余空间
EXACTLYwrap_contentAT_MOST父剩余空间
AT_MOST具体值 dpEXACTLY具体值
AT_MOSTmatch_parentAT_MOST父剩余空间
AT_MOSTwrap_contentAT_MOST父剩余空间
UNSPECIFIED具体值 dpEXACTLY具体值
UNSPECIFIEDmatch_parentUNSPECIFIED0
UNSPECIFIEDwrap_contentUNSPECIFIED0

看懂这张表,你就明白为什么自定义 View 不处理AT_MOST时wrap_content会变成撑满——因为默认getDefaultSize在AT_MOST下直接返回了specSize,也就是父容器给的剩余空间。要支持wrap_content,必须在onMeasure里自己判断模式并给出期望尺寸。

4. 可复制的 onMeasure 模板与日志验证测量次数

理论讲完,直接上能跑的模板。下面这个自定义 View 同时支持match_parent、wrap_content和具体数值,是自定义 View 的标准写法:

public class SquareTextView extends View { private static final String TAG = "MeasureDemo"; private static final int DEFAULT_SIZE = 200; // px private Paint mPaint; private String mText = "Measure"; public SquareTextView(Context context, AttributeSet attrs) { super(context, attrs); mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setTextSize(40); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); Log.d(TAG, "onMeasure widthMode=" + modeToString(widthMode) + " widthSize=" + widthSize + " heightMode=" + modeToString(heightMode) + " heightSize=" + heightSize); int desiredWidth = (int) mPaint.measureText(mText) + getPaddingLeft() + getPaddingRight(); int desiredHeight = (int) mPaint.getFontMetrics().descent - (int) mPaint.getFontMetrics().ascent + getPaddingTop() + getPaddingBottom(); int finalWidth = resolveSize(desiredWidth, widthMeasureSpec); int finalHeight = resolveSize(desiredHeight, heightMeasureSpec); setMeasuredDimension(finalWidth, finalHeight); } private String modeToString(int mode) { switch (mode) { case MeasureSpec.EXACTLY: return "EXACTLY"; case MeasureSpec.AT_MOST: return "AT_MOST"; default: return "UNSPECIFIED"; } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawText(mText, getPaddingLeft(), getPaddingTop() - mPaint.ascent(), mPaint); } }

这里的关键是resolveSize,它内部就是按 MeasureSpec 模式做取舍:EXACTLY直接用specSize,AT_MOST取min(desiredSize, specSize),UNSPECIFIED用desiredSize。这样wrap_content才会返回文字实际宽度,而不是撑满父容器。

验证测量次数,用日志最直接。在布局里放这个 View,然后观察 Logcat:

<com.example.SquareTextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="8dp" />

第一次显示时你会看到类似输出:

D/MeasureDemo: onMeasure widthMode=AT_MOST widthSize=1080 heightMode=AT_MOST heightSize=1920

说明父容器(比如 LinearLayout)给的是AT_MOST,specSize是屏幕可用空间。此时resolveSize会返回文字实际宽度,wrap_content生效。如果你把layout_width改成match_parent,日志会变成widthMode=EXACTLY widthSize=1080,resolveSize直接返回 1080。

再验证「缓存」行为:在 Activity 里连续调用两次requestLayout(),你会发现onMeasure不一定被调用两次。因为View.measure里那个if判断会比对mOldWidthMeasureSpec,如果 MeasureSpec 没变且没有FORCE_LAYOUT,就直接跳过。这解释了为什么有时候改了数据但界面没刷新——你可能需要主动requestLayout()或invalidate()。

5. 常见报错排查:IllegalStateException 与 wrap_content 失效

measure 相关的坑集中在两类,对照真实报错逐个排查。

报错一:java.lang.IllegalStateException: onMeasure() did not set the measured dimension by calling setMeasuredDimension()

这个报错 100% 是因为你重写了onMeasure但没调用setMeasuredDimension。常见于自定义 ViewGroup,测量完子 View 后忘了设置自身大小。修复方式就是在onMeasure最后一行补上:

setMeasuredDimension(finalWidth, finalHeight);

注意setMeasuredDimension是final的,不能重写,只能调用。另外如果你在onMeasure里提前return,也要确保每条分支都调用了它。

报错二:wrap_content不生效,控件撑满父容器

这不是崩溃,但更常见。根因是默认onMeasure在AT_MOST模式下返回了specSize。修复方式就是第 4 节模板里的做法:自己计算desiredWidth/desiredHeight,再用resolveSize处理。如果你用的是ViewGroup,还要注意给子 View 传 MeasureSpec 时用measureChildWithMargins而不是直接child.measure(0, 0),后者会丢失父约束。

报错三:NullPointerException在getLayoutParams()

自定义 ViewGroup 里调用child.getLayoutParams()返回 null,通常是因为子 View 是动态addView进来的,还没走完布局流程。解决方式是重写generateDefaultLayoutParams()和checkLayoutParams(),保证每个子 View 都有合法的 LayoutParams。

报错四:测量结果和预期差一个 padding

getChildMeasureSpec里减去的padding是mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed。如果你自己算 MeasureSpec 时漏了 padding 或 margin,子 View 就会超出父容器。排查时把specSize、padding、childDimension三个值打日志,对照第 3 节的表格逐行核对。

报错五:onMeasure被调用次数异常多

如果日志里onMeasure刷屏,通常是布局嵌套过深或requestLayout被反复触发。检查是否有在onMeasure里调用requestLayout(绝对禁止),或者RecyclerView的 item 布局里用了wrap_content导致多次测量。用ViewDebug.TRACE_HIERARCHY打开层级追踪能定位到具体 View。

排查完这些,measure 链路基本就通了。如果你在接入模型能力做智能布局分析或代码辅助,需要稳定的 API 入口,可以看下 TaoToken 的接入文档,配合 API Keys 管理 用起来比较顺手。

6. 把 measure 链路用起来:从源码到自定义控件

回到最开始那个问题:为什么wrap_content会失效?现在你应该能完整回答了——因为父容器通过getChildMeasureSpec把AT_MOST和剩余空间传下来,而默认onMeasure在AT_MOST下直接用了specSize,没有尊重子 View 的「期望尺寸」。修复的关键就是在onMeasure里主动计算desiredSize并用resolveSize做取舍。

整条链路再串一遍:ViewRootImpl.performTraversals()触发 →performMeasure()从 DecorView 开始 →View.measure()判断是否需要重新测量 → 回调onMeasure()→ ViewGroup 通过measureChildWithMargins+getChildMeasureSpec把父约束换算成子约束 → 递归到叶子 View → 每个 View 用setMeasuredDimension写回mMeasuredWidth/mMeasuredHeight。measure 是 final 的,扩展点只有onMeasure;MeasureSpec 是约束的载体,三种模式对应三种布局意图。

实用技巧补两个。第一,调试测量问题时,在onMeasure里打印MeasureSpec.toString(widthMeasureSpec),它会把 mode 和 size 一起输出,比手动拆位方便。第二,自定义 ViewGroup 测量子 View 时,优先用measureChildWithMargins,它会自动处理 margin 和 padding,避免自己算错。第三,如果只是想让 View 支持wrap_content,用resolveSize就够了,不用手写 switch。

最后留一个可以自己动手验证的实验:把第 4 节的SquareTextView放进ScrollView,观察onMeasure的 mode 变化。你会发现ScrollView给子 View 的高度 MeasureSpec 是UNSPECIFIED,因为滚动容器不知道内容多高。这时候resolveSize会返回desiredHeight,正好验证了UNSPECIFIED模式的行为。跑通这个实验,measure 的约束传递模型你就真正掌握了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询