一、MeasureSpec 到底是什么?

说实话,我刚接触 Android 自定义 View 那会儿,看到 MeasureSpec 这个类,第一反应是「这玩意儿是干啥的?」。后来踩了不少坑才明白——MeasureSpec 就是 View 测量阶段的「通行证」,它告诉每个 View:「你爹希望你长多大」。

每个 View 在测量时,都会收到一个 MeasureSpec。这个值不是随便给的,而是由父 View 根据自身的 MeasureSpec 和子 View 的 LayoutParams 计算出来的。嗯,这里有个关键点:子 View 的尺寸不是自己说了算的,得看父 View 的脸色。

二、MeasureSpec 的组成:mode + size

MeasureSpec 是一个 32 位的 int 值。别被它吓到,其实结构很简单:

  • 高 2 位:代表测量模式(mode)
  • 低 30 位:代表具体尺寸(size)

用代码说话:

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

    // 三种模式常量
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    public static final int EXACTLY     = 1 << MODE_SHIFT;
    public static final int AT_MOST     = 2 << MODE_SHIFT;

    // 打包成 MeasureSpec
    public static int makeMeasureSpec(int size, int mode) {
        return (size & ~MODE_MASK) | (mode & MODE_MASK);
    }

    // 解包获取 mode
    public static int getMode(int measureSpec) {
        return measureSpec & MODE_MASK;
    }

    // 解包获取 size
    public static int getSize(int measureSpec) {
        return measureSpec & ~MODE_MASK;
    }
}

你看,makeMeasureSpec 把 mode 和 size 塞进一个 int 里,getModegetSize 再拆出来。为什么这么设计?说白了就是为了性能——减少对象创建,一个 int 搞定所有。

💡 我的习惯: 调试自定义 View 时,我经常用 MeasureSpec.toString() 打印出来看。比如 "MeasureSpec: EXACTLY 1080",一眼就知道父 View 给了什么约束。

三、三种模式详解

1. UNSPECIFIED(未指定模式)

父 View 对子 View 没有任何限制。子 View 想要多大就多大。听起来很自由对吧?但实际开发中很少遇到。

什么时候出现?

  • ScrollView 或 ListView 在测量子 View 时,可能会传 UNSPECIFIED
  • 系统内部的一些测量场景,比如 AdapterView 的测量

举个例子:

// 父 View 传了个 UNSPECIFIED,size 是 0
int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
// 子 View 可以尽情伸展,想多大就多大
⚠️ 我曾经踩过的坑: 在自定义 ViewGroup 里,如果对子 View 传了 UNSPECIFIED,子 View 可能会变得非常大,导致内存溢出。所以除非你明确知道自己在做什么,否则别轻易用 UNSPECIFIED。

2. EXACTLY(精确模式)

父 View 给子 View 定死了尺寸。子 View 必须在这个尺寸内绘制,不能越界。

什么时候出现?

  • 子 View 的 layout_width 或 layout_height 设为 match_parent 或具体 dp 值
  • 父 View 已经确定了子 View 的最终尺寸

代码示例:

// 父 View 说:你就给我长 200px,别废话
int spec = MeasureSpec.makeMeasureSpec(200, MeasureSpec.EXACTLY);
// 子 View 的 onMeasure 里,getSize(spec) 返回 200

你想想看,match_parent 为什么是 EXACTLY?因为父 View 已经知道自己有多大,它告诉子 View:「我的剩余空间就这么多,你填满它」。说白了,尺寸是确定的。

3. AT_MOST(最大模式)

父 View 给了一个上限:你最大不能超过这个值,但可以比它小。

什么时候出现?

  • 子 View 的 layout_width 或 layout_height 设为 wrap_content
  • 父 View 不确定子 View 到底要多大,但给了个最大值

代码示例:

// 父 View 说:你最大 300px,但可以更小
int spec = MeasureSpec.makeMeasureSpec(300, MeasureSpec.AT_MOST);
// 子 View 测量后,如果内容只需要 100px,那最终就是 100px

这里有个细节:wrap_content 对应 AT_MOST。但如果你在自定义 View 里不处理 AT_MOST,直接调 super.onMeasure(),那效果就跟 match_parent 一样了。嗯,这也是很多新手踩坑的地方。

四、子 View 的 MeasureSpec 如何由父 View 决定?

这是整个测量流程的核心。父 View 在 onMeasure() 里遍历子 View 时,会调用 getChildMeasureSpec() 方法来计算每个子 View 的 MeasureSpec。

咱们直接看源码(简化版):

public static int getChildMeasureSpec(int parentSpec, int padding, int childDimension) {
    int parentMode = MeasureSpec.getMode(parentSpec);
    int parentSize = MeasureSpec.getSize(parentSpec);

    // 父 View 可用空间 = 父 View 尺寸 - padding
    int resultSize = 0;
    int resultMode = 0;

    switch (parentMode) {
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                // 子 View 有固定尺寸
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // 子 View 想填满父 View
                resultSize = parentSize - padding;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // 子 View 想包裹内容
                resultSize = parentSize - padding;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = parentSize - padding;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = parentSize - padding;
                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);
}

这段代码看着复杂,其实逻辑很清晰。我总结成一张表:

父 View 模式 子 View LayoutParams 子 View 得到的 MeasureSpec
EXACTLY 固定 dp 值 EXACTLY + 固定值
MATCH_PARENT EXACTLY + 父剩余空间
WRAP_CONTENT AT_MOST + 父剩余空间
AT_MOST 固定 dp 值 EXACTLY + 固定值
MATCH_PARENT AT_MOST + 父剩余空间
WRAP_CONTENT AT_MOST + 父剩余空间
UNSPECIFIED 固定 dp 值 EXACTLY + 固定值
MATCH_PARENT UNSPECIFIED + 0
WRAP_CONTENT UNSPECIFIED + 0

注意看一个规律:只要子 View 给了固定尺寸,不管父 View 是什么模式,子 View 得到的都是 EXACTLY。这说明什么?说明子 View 的「意愿」在固定尺寸时是最高优先级。

核心结论:

  • 子 View 的 MeasureSpec 由「父 View 的 MeasureSpec」和「子 View 的 LayoutParams」共同决定
  • 父 View 的 padding 会影响子 View 的可用空间
  • 子 View 的 margin 在 getChildMeasureSpec 里不处理,是在 layout 阶段才考虑的

五、知识体系总览

下面这张图把 MeasureSpec 的核心逻辑串起来了:

MeasureSpec 知识体系 MeasureSpec (32位 int) 高2位:mode(模式) 低30位:size(尺寸) UNSPECIFIED:无限制 EXACTLY:精确尺寸(match_parent / 固定dp) AT_MOST:最大限制(wrap_content) 子 View MeasureSpec = f(父 View MeasureSpec, 子 View LayoutParams) 父 View 的 padding 会扣除,子 View 的 margin 在 layout 阶段处理

这张图把 MeasureSpec 的组成、三种模式、以及子 View MeasureSpec 的生成规则串在了一起。我个人建议你把它保存下来,每次写自定义 View 时对照着看,思路会清晰很多。

六、避坑指南

最后分享几个我实际项目中遇到的坑:

  1. 自定义 View 不处理 AT_MOST:如果你继承 View 重写 onMeasure,但忘了处理 AT_MOST 模式,直接调 super.onMeasure(),那 wrap_content 的效果就跟 match_parent 一样。我曾经因为这个被测试小姐姐追着改 bug……
  2. UNSPECIFIED 的滥用:在自定义 ViewGroup 里,如果对子 View 传 UNSPECIFIED,子 View 可能会测量出非常大的尺寸。比如一个 TextView 内容很长,UNSPECIFIED 下它会全部展示,导致内存飙升。
  3. padding 和 margin 的混淆:getChildMeasureSpec 只处理父 View 的 padding,不处理子 View 的 margin。子 View 的 margin 是在 layout 阶段才减掉的。所以你在 onMeasure 里看到的 size 是包含 margin 的,别搞混了。
📌 我的调试小技巧: 在 onMeasure 里加一行 Log.d("ViewMeasure", "MeasureSpec: " + MeasureSpec.toString(measureSpec)),然后跑一下布局,看看不同 LayoutParams 下 MeasureSpec 的变化。实践出真知。

好了,MeasureSpec 的核心内容就这些。记住一句话:MeasureSpec 是父 View 给子 View 的「尺寸指令」,理解了这个,自定义 View 的测量阶段你就掌握了 80%。


公众号:蓝海资料掘金营,微信deep3321