一、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 里,getMode 和 getSize 再拆出来。为什么这么设计?说白了就是为了性能——减少对象创建,一个 int 搞定所有。
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 可以尽情伸展,想多大就多大
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 的组成、三种模式、以及子 View MeasureSpec 的生成规则串在了一起。我个人建议你把它保存下来,每次写自定义 View 时对照着看,思路会清晰很多。
六、避坑指南
最后分享几个我实际项目中遇到的坑:
- 自定义 View 不处理 AT_MOST:如果你继承 View 重写 onMeasure,但忘了处理 AT_MOST 模式,直接调 super.onMeasure(),那 wrap_content 的效果就跟 match_parent 一样。我曾经因为这个被测试小姐姐追着改 bug……
- UNSPECIFIED 的滥用:在自定义 ViewGroup 里,如果对子 View 传 UNSPECIFIED,子 View 可能会测量出非常大的尺寸。比如一个 TextView 内容很长,UNSPECIFIED 下它会全部展示,导致内存飙升。
- padding 和 margin 的混淆:getChildMeasureSpec 只处理父 View 的 padding,不处理子 View 的 margin。子 View 的 margin 是在 layout 阶段才减掉的。所以你在 onMeasure 里看到的 size 是包含 margin 的,别搞混了。
好了,MeasureSpec 的核心内容就这些。记住一句话:MeasureSpec 是父 View 给子 View 的「尺寸指令」,理解了这个,自定义 View 的测量阶段你就掌握了 80%。
公众号:蓝海资料掘金营,微信deep3321