请描述 View 的 measure 过程中 SpecMode 的三种类型及其含义
解读
在国内大厂一面/二面中,这道题属于“必背基础题”,但面试官真正想听的是:
- 你能否把“模式”与“实际开发场景”对上号;
- 你能否说出“为什么”而不是“是什么”;
- 你能否把 measure 流程与性能、适配、折叠屏等热点联系起来。
只背“三句话”会被追问“那如果父布局是 ConstraintLayout 且尺寸是 0dp,对应哪种模式?”——答不上来就凉了。
知识点
- MeasureSpec 是一个 32 位 int,高 2 位存 mode,低 30 位存 size;
- 三种 mode 由父布局的 LayoutParams 与父布局自身可用空间共同决定,源码在 ViewGroup.getChildMeasureSpec() 里;
- 一次 measure 可能触发多次 onMeasure,尤其是复用池、动画、折叠屏旋转时;
- 错误使用 mode 会导致“测量-布局”震荡,直接掉帧;
- 自定义 View 必须在 onMeasure 里调用 setMeasuredDimension,否则抛 IllegalStateException。
答案
-
EXACTLY(精确值模式)
含义:父布局已经给子 View 一个确切尺寸,子 View 必须用这个尺寸。
触发场景:- 布局宽高写死 100dp;
- match_parent 且父布局本身尺寸确定;
- ConstraintLayout 里宽度设为 0dp 但横向约束拉满,最终计算出的宽度也是 EXACTLY。
自定义 View 处理:直接 setMeasuredDimension(specSize, specSize),无需二次计算。
-
AT_MOST(最大边界模式)
含义:父布局告诉子 View“你不能超过这个尺寸”,但具体多大由子 View 自己根据内容决定。
触发场景:- wrap_content;
- 父布局是 ScrollView,子 View 高度可以任意,但不能超过屏幕高度;
- 折叠屏展开后,父布局宽度变大,子 View 宽度可以“撑”但不得超出父布局剩余空间。
自定义 View 处理:先测量内容所需最小尺寸,再用 Math.min(contentSize, specSize) 决定最终值,避免“内容超长导致滑动冲突”。
-
UNSPECIFIED(无约束模式)
含义:父布局对子 View 尺寸没有任何上限要求,子 View 想多大就多大。
触发场景:- ListView、RecyclerView 在测量子项时,先走一次 UNSPECIFIED 探查“你到底有多高”,再决定是否需要复用或开启滚动;
- 自定义滚动容器、PathMeasure 动画路径测量;
- 系统键盘弹出时,DecorView 对根布局的第一次探测。
自定义 View 处理:必须返回“内容真实尺寸”,否则后续滚动、动画、折叠屏切换都会出现“跳变”或“空白”。
性能注意:若在此模式里做耗时计算(如加载大图),会阻塞 UI 线程,16 ms 内无法完成就会掉帧。
拓展思考
- 折叠屏适配:展开后外层 FrameLayout 宽度从 400dp→800dp,子 View 的 MeasureSpec 由 AT_MOST 变为 EXACTLY,若自定义 View 缓存了旧 bitmap,必须监听 onConfigurationChanged 重新采样,否则内存暴涨。
- 懒加载优化:RecyclerView 预加载时先以 UNSPECIFIED 探测,若 item 高度>屏幕,会触发二次测量,导致预加载失效;可通过重写 LayoutManager 的 measureChildWithMargins,提前限制 heightSpec 为 AT_MOST 屏幕高,减少一次 measure。
- Compose 的 MeasurePolicy 虽然 API 变了,但内部仍复用 MeasureSpec 三种语义;理解 View 体系的 mode,有助于在 Compose 里写出正确的 IntrinsicMeasurable,避免 SubcomposeLayout 出现无限测量循环。