请描述 View 的 measure 过程中 SpecMode 的三种类型及其含义

解读

在国内大厂一面/二面中,这道题属于“必背基础题”,但面试官真正想听的是:

  1. 你能否把“模式”与“实际开发场景”对上号;
  2. 你能否说出“为什么”而不是“是什么”;
  3. 你能否把 measure 流程与性能、适配、折叠屏等热点联系起来。
    只背“三句话”会被追问“那如果父布局是 ConstraintLayout 且尺寸是 0dp,对应哪种模式?”——答不上来就凉了。

知识点

  1. MeasureSpec 是一个 32 位 int,高 2 位存 mode,低 30 位存 size;
  2. 三种 mode 由父布局的 LayoutParams 与父布局自身可用空间共同决定,源码在 ViewGroup.getChildMeasureSpec() 里;
  3. 一次 measure 可能触发多次 onMeasure,尤其是复用池、动画、折叠屏旋转时;
  4. 错误使用 mode 会导致“测量-布局”震荡,直接掉帧;
  5. 自定义 View 必须在 onMeasure 里调用 setMeasuredDimension,否则抛 IllegalStateException。

答案

  1. EXACTLY(精确值模式)
    含义:父布局已经给子 View 一个确切尺寸,子 View 必须用这个尺寸。
    触发场景:

    • 布局宽高写死 100dp;
    • match_parent 且父布局本身尺寸确定;
    • ConstraintLayout 里宽度设为 0dp 但横向约束拉满,最终计算出的宽度也是 EXACTLY。
      自定义 View 处理:直接 setMeasuredDimension(specSize, specSize),无需二次计算。
  2. AT_MOST(最大边界模式)
    含义:父布局告诉子 View“你不能超过这个尺寸”,但具体多大由子 View 自己根据内容决定。
    触发场景:

    • wrap_content;
    • 父布局是 ScrollView,子 View 高度可以任意,但不能超过屏幕高度;
    • 折叠屏展开后,父布局宽度变大,子 View 宽度可以“撑”但不得超出父布局剩余空间。
      自定义 View 处理:先测量内容所需最小尺寸,再用 Math.min(contentSize, specSize) 决定最终值,避免“内容超长导致滑动冲突”。
  3. UNSPECIFIED(无约束模式)
    含义:父布局对子 View 尺寸没有任何上限要求,子 View 想多大就多大。
    触发场景:

    • ListView、RecyclerView 在测量子项时,先走一次 UNSPECIFIED 探查“你到底有多高”,再决定是否需要复用或开启滚动;
    • 自定义滚动容器、PathMeasure 动画路径测量;
    • 系统键盘弹出时,DecorView 对根布局的第一次探测。
      自定义 View 处理:必须返回“内容真实尺寸”,否则后续滚动、动画、折叠屏切换都会出现“跳变”或“空白”。
      性能注意:若在此模式里做耗时计算(如加载大图),会阻塞 UI 线程,16 ms 内无法完成就会掉帧。

拓展思考

  1. 折叠屏适配:展开后外层 FrameLayout 宽度从 400dp→800dp,子 View 的 MeasureSpec 由 AT_MOST 变为 EXACTLY,若自定义 View 缓存了旧 bitmap,必须监听 onConfigurationChanged 重新采样,否则内存暴涨。
  2. 懒加载优化:RecyclerView 预加载时先以 UNSPECIFIED 探测,若 item 高度>屏幕,会触发二次测量,导致预加载失效;可通过重写 LayoutManager 的 measureChildWithMargins,提前限制 heightSpec 为 AT_MOST 屏幕高,减少一次 measure。
  3. Compose 的 MeasurePolicy 虽然 API 变了,但内部仍复用 MeasureSpec 三种语义;理解 View 体系的 mode,有助于在 Compose 里写出正确的 IntrinsicMeasurable,避免 SubcomposeLayout 出现无限测量循环。