RelativeLayout测量优化失效

RelativeLayout为什么会测量优化失效,影响性能

1.什么是 measure 优化?

Android 的 View.measure() 方法中有一段优化逻辑:

if (oldMeasureSpec == newMeasureSpec) {
    // 👇 说明父容器传给我的宽高没有变
    // 我就不重新测量了,直接复用上次的结果,节省性能
    return;
}

🔧 所以:只要父容器每次给我的 MeasureSpec 是一样的,我就能跳过测量

这就是 Android 布局系统的一种 缓存机制(跳过 measure),提高性能非常关键。

2. LinearLayout 是怎么测量的?(简单直接)

以垂直方向为例,它会:

  1. 从上往下一个一个测量子 View;
  2. 子 View 的高度用 wrap_content → LinearLayout 传入的是 AT_MOST;
  3. 每次测量后累加总高度;
  4. 所有子 View 都测完后,确定自己的总高度。

💡 重点:只测一次,每个子 View 的 MeasureSpec 都稳定(父宽固定,高是 AT_MOST),所以几乎所有子 View 的测量都可以缓存(不会重复 measure)。

3. RelativeLayout 是怎么测量的?(横纵分离)

由于 RelativeLayout 的子 View 可能依赖于彼此的位置(比如 A 在 B 右边、B 在 A 下方),所以:

它需要两轮测量:

  • 第一轮:横向测量

    • 确定所有子 View 的左右坐标
    • 但此时还没确定每个 View 的高度 → 只能先“临时用一个高度”去测
  • 第二轮:纵向测量

    • 重新测量子 View 的实际高度
    • 此时才能算出 RelativeLayout 的高度

4. 问题出在“第一次传进去的高度是假的

比如:

applyHorizontalSizeRules(params, myWidth, rules);
measureChildHorizontal(child, params, myWidth, myHeight); // ⚠️ 这里 myHeight 是假的
  • myHeight 是父 View 当前的高度(比如 wrap_content,还没定好);
  • 如果子 View 设置了 marginBottom 等属性,它的测量高度就会 = myHeight - marginBottom;
  • 第二轮再测一次,传进去的新 MeasureSpec ≠ 上一次的;
  • measure 优化就失效了,子 View 被迫重新测量

举个类比更直观:

想象你在量窗帘的宽度:

  • 你第一次预估窗子高 2 米;
  • 把窗帘量了一遍,觉得布料要剪 1.8 米;
  • 后来你发现窗子其实有 2.2 米高,你得重新量布料;
  • ➤ 白白多测了一遍,因为第一次“假的高度”造成了浪费。

所以关键点是:

问题 原因
RelativeLayout 子 View 被重复测量 第一次测量用的是不准确的高度
优化失效 MeasureSpec 不一致,不能跳过 measure
与 margin 有关系 marginBottom、marginTop 会影响计算,造成误差
LinearLayout 没这问题 顺序测量,MeasureSpec 是真实的,不跳过也不会错
建议用 padding 替代 margin padding 不影响测量,margin 是 layout param,参与 measureSpec 的计算逻辑

总结

RelativeLayout 因为要分两轮测量(横 + 纵),第一次传入子 View 的高度是假的,如果子 View 设置了 margin,就会导致第二轮测量时发现 MeasureSpec 变了,无法复用第一次结果,measure 优化失效,从而影响性能。

如何避免

  • 避免 RelativeLayout 嵌套太深;
  • 尽量减少使用 margin,特别是 layout_marginBottom、layout_marginTop;
  • 尽量用 padding 实现留白;
  • 如果用的是新项目,建议使用 ConstraintLayout(一次测量,无重复);

理解

在RelativeLayout 的第一次横向测量中,系统会调用:

measureChildHorizontal(child, params, myWidth, myHeight);

此时传入的 myHeight 是一个临时值,尚未准确计算,也未考虑子 View 的 marginTop、marginBottom。

接下来:

在第二轮纵向测量中,会调用:

measureChild(child, params, myWidth, myHeight);

这一次系统已经知道该考虑 margin,于是:
• 计算:childHeight = myHeight - params.topMargin - params.bottomMargin
• 然后调用:

getChildMeasureSpec(parentHeightSpec, margin, LayoutParams.WRAP_CONTENT)

此时生成的 MeasureSpec 和第一次不同。

于是问题来了:
• 子 View 收到 不同的 MeasureSpec(虽然 mode 是 AT_MOST,但 size 变了);
• 系统为了安全起见,会重新调用 onMeasure();
• ➤ 这就打破了原本的 measure 优化逻辑!

第一次传递进去的是 myHeight(没减 margin)
第二次传进去的是 myHeight - margin
➤ 因此可能导致系统判定 MeasureSpec 改变 → 重新触发子 View 测量(measure)

所以为什么 LinearLayout 没这个问题?

因为它 只有一次测量,从一开始就把 margin 计入了:

childHeight = parentHeight - usedHeightSoFar - lp.topMargin - lp.bottomMargin

没有“中途修正”的概念,所以 measureSpec 更稳定,能更好复用测量结果。

measureChildHorizontal()

详细分析 RelativeLayout.measureChildHorizontal() 方法,看它如何在第一次横向测量中处理子View的测量逻辑,以及它为何会传入一个“未考虑 margin 的 myHeight”。

函数背景

private void measureChildHorizontal(View child, LayoutParams params, int myWidth, int myHeight)

这是 RelativeLayout 专门用于第一轮“横向测量”的内部方法,调用发生在:

// 横向第一轮测量(width)
applyHorizontalSizeRules(params, myWidth, rules);
measureChildHorizontal(child, params, myWidth, myHeight);

作用

这一步不是为了测量高度,而是为了测量宽度和设置左右位置(mLeft、mRight)

但由于 View.measure(widthSpec, heightSpec) 必须同时传入宽高规格(即使暂时不关心某一项),所以这里仍然要传入一个 heightSpec。

问题就在于传入的 height 是 myHeight,并没有考虑 topMargin 和 bottomMargin,也不是最终的高度。

实际流程(核心步骤)

我们基于 Android 源码简化整理(省略对 RTL 的处理):

1. 构造 MeasureSpec

final int childWidthMeasureSpec = getChildMeasureSpec(
    MeasureSpec.makeMeasureSpec(myWidth, MeasureSpec.EXACTLY),
    paddingLeft + paddingRight + params.leftMargin + params.rightMargin,
    params.width);

final int childHeightMeasureSpec = getChildMeasureSpec(
    MeasureSpec.makeMeasureSpec(myHeight, MeasureSpec.EXACTLY), // ⚠️注意:未减 margin
    paddingTop + paddingBottom, // 也没有加入 topMargin/bottomMargin
    params.height);

2. 调用子 View 的 measure

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

这就完成了“第一轮”对该子 View 的测量。

但此时高度的 MeasureSpec 是基于“父容器的 myHeight”,而不是“真实剩余高度”,也未考虑子 View 的 top/bottomMargin

问题点在哪?

当 RelativeLayout 第二轮开始纵向测量时,它会这样处理:

applyVerticalSizeRules(params, myHeight);
measureChild(child, params, myWidth, myHeight); // ⚠️再次测量

这时候:

  • myHeight 可能更精确(比如 wrap_content 计算后的高度);
  • getChildMeasureSpec() 也会考虑 params.topMargin 和 params.bottomMargin;
  • ➤ 构造出的 MeasureSpec 与第一次不同,系统判定为“必须重新测量”。

为什么 LinearLayout 没这个问题?

LinearLayout 没有 child 之间的依赖,也没有“分轮测量”的需求,所以:

  • 只测一次;
  • 测量时就会传入正确的 height(含 margin);
  • ➤ 系统能稳定命中缓存,性能更好。

总结:

measureChildHorizontal()

的问题本质

特点 描述
功能 为子 View 测量宽度 & 设置横向位置(mLeft、mRight)
高度传入 myHeight,未考虑 margin,不是准确值
问题 第二次测量时 MeasureSpec 不一致,导致子 View 被再次 measure
性能影响 measure 优化失效,增加 layout pass 开销

measureChild(child, params, myWidth, myHeight);

这是用于 纵向测量 的阶段,重点是测量子 View 的高度,并在此基础上计算其纵向布局位置(如 top、bottom),从而得出整个 RelativeLayout 的真实高度。

这个方法在哪定义?

你引用的 measureChild(…) 是 RelativeLayout 中的私有方法,不是 ViewGroup.measureChild() 的那个通用版本。

下面我们基于 Android AOSP 源码来拆解这个私有版本的实现逻辑(核心代码):

函数目的

为每个子 View 生成合适的 MeasureSpec(宽高),然后调用它的 measure() 方法,让子 View 知道自己该有多大。

源码结构(伪代码简化)

private void measureChild(View child, LayoutParams params, int myWidth, int myHeight) {
    final int childWidthMeasureSpec = getChildMeasureSpec(
        MeasureSpec.makeMeasureSpec(myWidth, MeasureSpec.EXACTLY),
        mPaddingLeft + mPaddingRight + params.leftMargin + params.rightMargin,
        params.width);

    final int childHeightMeasureSpec = getChildMeasureSpec(
        MeasureSpec.makeMeasureSpec(myHeight, MeasureSpec.EXACTLY),
        mPaddingTop + mPaddingBottom + params.topMargin + params.bottomMargin,
        params.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

每个参数的意义

参数 说明
myWidth 父容器(RelativeLayout)的宽度,已确定
myHeight 父容器最终确定的高度,用来推断子 View 的可用高度
params.width 子 View 的 layout_width,如 match_parent/wrap_content/具体dp
params.height 子 View 的 layout_height,同上
getChildMeasureSpec(…) 核心函数,生成传给子 View 的 MeasureSpec(mode + size)

*和measureChildHorizontal()的关键差异:

比较项 measureChildHorizontal measureChild
目的 测量宽度,位置是 mLeft/mRight 测量高度,位置是 mTop/mBottom
高度参数 传入的是 myHeight(临时值),未考虑 margin 传入的是最终 myHeight,含 margin
高度是否准确 ❌ 否,临时/错误 ✅ 是,真实
是否可能引发 measure 重进 ✅ 可能 ❌ 通常不会

性能陷阱出现在哪?

因为在 measureChildHorizontal() 中子 View 早就被测了一次:

child.measure(widthSpec, fakeHeightSpec); // 用的错误的 myHeight

等到第二轮调用 measureChild(…):

  • 系统再测一次;
  • 因为 heightSpec 不一样了(现在是准确高度、考虑了 margin);
  • ➤ 系统无法命中缓存,只能 重新走一遍 onMeasure()

这就破坏了系统对 View 的 测量优化策略,影响性能,尤其在嵌套复杂布局时(如嵌套 ScrollView、图片、大文本等)。

最终小结

阶段 方法 高度准确性 是否优化命中 说明
第一次横向测量 measureChildHorizontal() ❌ 不准确 ❌ 不能命中 用于先确定左右位置,传入的是临时 myHeight
第二次纵向测量 measureChild() ✅ 准确 ❌(和上次不同) 传入的是完整 myHeight + margin,重新测量

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器