Android measure 过程详解:结合 4.0.4 源码看 View、MeasureSpec 与 onMeasure 的协作

发布时间:2026/10/4 9:09:17
Android measure 过程详解:结合 4.0.4 源码看 View、MeasureSpec 与 onMeasure 的协作 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 为什么是 finalmeasure 的起点不在 View 自己而在ViewRootImpl.performTraversals()。当界面需要刷新时ViewRootImpl 会依次判断是否需要 measure、layout、draw其中 measure 是前提——因为 layout 要用到测量结果draw 又要用到 layout 确定的位置。所以整条链路是ViewRootImpl.performTraversals()→performMeasure()→mView.measure(childWidthMeasureSpec, childHeightMeasureSpec)从根 ViewDecorView开始向下递归。进入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父约束如何传到子 Viewmeasure(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 也算进去。核心逻辑在measureChildWithMarginsprotected 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()。真正的换算逻辑在getChildMeasureSpecpublic 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结果 sizeEXACTLY具体值 dpEXACTLY具体值EXACTLYmatch_parentEXACTLY父剩余空间EXACTLYwrap_contentAT_MOST父剩余空间AT_MOST具体值 dpEXACTLY具体值AT_MOSTmatch_parentAT_MOST父剩余空间AT_MOSTwrap_contentAT_MOST父剩余空间UNSPECIFIED具体值 dpEXACTLY具体值UNSPECIFIEDmatch_parentUNSPECIFIED0UNSPECIFIEDwrap_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直接用specSizeAT_MOST取min(desiredSize, specSize)UNSPECIFIED用desiredSize。这样wrap_content才会返回文字实际宽度而不是撑满父容器。验证测量次数用日志最直接。在布局里放这个 View然后观察 Logcatcom.example.SquareTextView android:layout_widthwrap_content android:layout_heightwrap_content android:padding8dp /第一次显示时你会看到类似输出D/MeasureDemo: onMeasure widthModeAT_MOST widthSize1080 heightModeAT_MOST heightSize1920说明父容器比如 LinearLayout给的是AT_MOSTspecSize是屏幕可用空间。此时resolveSize会返回文字实际宽度wrap_content生效。如果你把layout_width改成match_parent日志会变成widthModeEXACTLY widthSize1080resolveSize直接返回 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。报错四测量结果和预期差一个 paddinggetChildMeasureSpec里减去的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 通过measureChildWithMarginsgetChildMeasureSpec把父约束换算成子约束 → 递归到叶子 View → 每个 View 用setMeasuredDimension写回mMeasuredWidth/mMeasuredHeight。measure 是 final 的扩展点只有onMeasureMeasureSpec 是约束的载体三种模式对应三种布局意图。实用技巧补两个。第一调试测量问题时在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 的约束传递模型你就真正掌握了。