为什么 RelativeLayout 的效率比 LinearLayout / FrameLayout 低

为什么 RelativeLayout 的效率比 LinearLayout / FrameLayout 低?

1. 布局的计算复杂度不同

  • LinearLayout:布局规则简单。只需要按顺序(横向/纵向)测量和摆放子 View。它的布局过程基本是 线性遍历子元素 + 简单测量和摆放

  • FrameLayout:更简单,所有子 View 都默认叠加在左上角,只看 layout_gravity 就能控制摆放。

  • RelativeLayout:需要根据每个子 View 的布局依赖(如:layout_toRightOf、layout_alignParentTop)去动态计算各 View 的位置,这意味着:

    • 可能需要 多次遍历子 View
    • 要解决 依赖图,排布顺序必须按依赖关系进行;
    • 需要构建和维护一个 布局依赖图(如 A 在 B 右边,B 又依赖 C 等)。

🔧 结论:RelativeLayout 的测量和布局阶段更复杂,会进行多轮计算、排序、依赖解析,而 LinearLayout 和 FrameLayout 则非常直接。

2.绘制开销差异

虽然布局阶段的复杂性是主要影响因素,但也会间接影响绘制:

  • Android 中 View 的测量(measure)、布局(layout)、绘制(draw)是按层级递归完成的。
  • 布局层级越深、布局依赖越多,CPU 和 GPU 的负担都会增加
  • RelativeLayout 虽然是单层,但其复杂性可能抵得上多个简单布局的组合。

3真实性能数据

Google 在早期的 Android 优化推荐中,曾经建议尽量避免使用 RelativeLayout,原因就是它:

  • 布局耗时较长
  • 不易优化
  • 对嵌套的子布局优化空间小

虽然 ConstraintLayout 后来在一定程度上替代了 RelativeLayout,但相比之下,LinearLayout+权重/嵌套,或 FrameLayout 简单叠加,在大多数场景下效率更高。

最佳实践总结

场景 推荐布局 原因
简单线性排列(水平或垂直) LinearLayout 简单高效,低开销
仅需要层叠(如 banner + 遮罩) FrameLayout 结构简单
必须满足复杂相对定位关系 RelativeLayout 可避免嵌套,取舍用
多个 View 有依赖且避免嵌套层级 ConstraintLayout 更现代的替代方案(性能好于 RelativeLayout)

最佳建议

  • 相比 RelativeLayout,使用 ConstraintLayout 可以通过构建更合理的依赖图,一次测量完成所有布局(它内部使用了线性方程求解器)。
  • 在简单场景下优先用 LinearLayout 或 FrameLayout,避免额外测量和依赖排序开销

RelativeLayout的onMeasure()方法中的核心逻辑

View[] views = mSortedHorizontalChildren;
    int count = views.length;

    for (int i = 0; i < count; i++) {
      View child = views[i];
      if (child.getVisibility() != GONE) {
        LayoutParams params = (LayoutParams) child.getLayoutParams();
        int[] rules = params.getRules(layoutDirection);

        applyHorizontalSizeRules(params, myWidth, rules);
        measureChildHorizontal(child, params, myWidth, myHeight);

        if (positionChildHorizontal(child, params, myWidth, isWrapContentWidth)) {
          offsetHorizontalAxis = true;
        }
      }
    }

    views = mSortedVerticalChildren;
    count = views.length;
    final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;

    for (int i = 0; i < count; i++) {
      View child = views[i];
      if (child.getVisibility() != GONE) {
        LayoutParams params = (LayoutParams) child.getLayoutParams();
       
        applyVerticalSizeRules(params, myHeight);
        measureChild(child, params, myWidth, myHeight);
        if (positionChildVertical(child, params, myHeight, isWrapContentHeight)) {
          offsetVerticalAxis = true;
        }

        if (isWrapContentWidth) {
          if (isLayoutRtl()) {
            if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
              width = Math.max(width, myWidth - params.mLeft);
            } else {
              width = Math.max(width, myWidth - params.mLeft - params.leftMargin);
            }
          } else {
            if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
              width = Math.max(width, params.mRight);
            } else {
              width = Math.max(width, params.mRight + params.rightMargin);
            }
          }
        }

        if (isWrapContentHeight) {
          if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
            height = Math.max(height, params.mBottom);
          } else {
            height = Math.max(height, params.mBottom + params.bottomMargin);
          }
        }

        if (child != ignore || verticalGravity) {
          left = Math.min(left, params.mLeft - params.leftMargin);
          top = Math.min(top, params.mTop - params.topMargin);
        }

        if (child != ignore || horizontalGravity) {
          right = Math.max(right, params.mRight + params.rightMargin);
          bottom = Math.max(bottom, params.mBottom + params.bottomMargin);
        }
      }
    }

这段代码是 Android RelativeLayout 的 onMeasure() 方法中的核心逻辑部分,负责对子 View 的 宽高进行测量,它展示了 RelativeLayout 为什么会对 View 执行两次 measure 的关键代码路径

按照执行顺序,分两大块:横向(horizontal)+ 纵向(vertical),逐句解释:

一、第一阶段:测量水平方向(宽度)

View[] views = mSortedHorizontalChildren;
int count = views.length;
  • mSortedHorizontalChildren 是 RelativeLayout 内部根据横向依赖关系排好序的子 View 数组(A 依赖 B → B 必在 A 之前);

  • 这是为了解决“某个 View 依赖另一个 View 的位置”的问题,保证顺序正确。

    for (int i = 0; i < count; i++) {
    View child = views\[i];
    if (child.getVisibility() != GONE) {
    LayoutParams params = (LayoutParams) child.getLayoutParams();
    int\[] rules = params.getRules(layoutDirection);
    
        applyHorizontalSizeRules(params, myWidth, rules);
        measureChildHorizontal(child, params, myWidth, myHeight);
    
        if (positionChildHorizontal(child, params, myWidth, isWrapContentWidth)) {
          offsetHorizontalAxis = true;
        }
    
    }
    

    }

关键方法解释:

  • applyHorizontalSizeRules(…)

    • 解析 layout_toRightOf、layout_alignParentLeft 等横向规则;
    • 计算 child 所需的左右边界(mLeft、mRight);
    • 但此时只是“初步预估”。
  • measureChildHorizontal(…)

    • 真正开始调用 child.measure(…);
    • 会给 child 一个明确的宽度 MeasureSpec;
    • 这是第一次 measure 调用,主要处理 child 的宽度
  • positionChildHorizontal(…)

    • 根据 mLeft 和 mRight 设置实际的位置值;
    • 如果是 wrap_content,可能导致偏移,需记录。

二、第二阶段:测量竖直方向(高度)

views = mSortedVerticalChildren;
count = views.length;
final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
  • mSortedVerticalChildren 同样是按照纵向依赖关系(如 layout_below)排序好的子 View 数组。

    for (int i = 0; i < count; i++) {
    View child = views[i];
    if (child.getVisibility() != GONE) {
    LayoutParams params = (LayoutParams) child.getLayoutParams();

    applyVerticalSizeRules(params, myHeight);
    measureChild(child, params, myWidth, myHeight);
    

重点:

  • applyVerticalSizeRules(…)

    • 解析 layout_below、alignParentTop 等竖直方向规则;
    • 初步计算 mTop 和 mBottom。
  • measureChild(…)

    • 第二次 measure 调用(竖直方向)
    • 它会考虑新的高度限制来重新测量 View;
    • 如果是 wrap_content,前面 measure 的宽度可能不足以估算高度,所以还需精确测量。

之后是动态更新整体尺寸:

if (isWrapContentWidth) {
  // 估算 wrap_content 情况下的父布局宽度
}

if (isWrapContentHeight) {
  // 同上,高度估算
}

这段代码会根据子 View 的实际 mRight、mBottom 等位置,动态计算父布局(RelativeLayout)本身的 wrap_content 尺寸。

总结:这段代码做了什么?

阶段 做了什么 是否调用 measure
横向排序遍历 根据依赖顺序调用 measureChildHorizontal() ✔️ 第一次 measure(测宽)
纵向排序遍历 调用 measureChild() 来确定高度 ✔️ 第二次 measure(测高)
位置更新 更新每个 View 的 left/top/right/bottom ❌ 仅计算,不调用 measure
尺寸估算 根据子 View 动态调整 RelativeLayout 的尺寸 ❌ 仅用来计算自身 wrap_content

所以最终你看到的:

  • 每个子 View 可能被调用了两次 measure:

    • 一次在横向遍历中;
    • 一次在纵向遍历中。
  • 这就是为什么 RelativeLayout 比 LinearLayout 性能更差的根源之一。

applyHorizontalSizeRules(params, myWidth, rules)

根据 XML 中设置的“横向依赖规则”(如 layout_toRightOf、layout_alignParentLeft 等),计算并更新子 View 的横向边界信息,主要是 LayoutParams.mLeft 和 mRight。

这些边界信息之后会被 measureChildHorizontal() 和 positionChildHorizontal() 使用,决定子 View 的宽度、位置等。

内部逻辑简要说明

(基于 AOSP 源码):

private void applyHorizontalSizeRules(LayoutParams childParams, int myWidth, int[] rules) {
    // 左边界
    if (rules[ALIGN_PARENT_LEFT] != 0) {
        childParams.mLeft = mPaddingLeft + childParams.leftMargin;
    } else if (rules[TO_RIGHT_OF] != 0) {
        View anchor = getRelatedView(rules[TO_RIGHT_OF]);
        if (anchor != null) {
            childParams.mLeft = anchor.mRight + anchor.rightMargin + childParams.leftMargin;
        }
    }

    // 右边界
    if (rules[ALIGN_PARENT_RIGHT] != 0) {
        childParams.mRight = myWidth - mPaddingRight - childParams.rightMargin;
    } else if (rules[TO_LEFT_OF] != 0) {
        View anchor = getRelatedView(rules[TO_LEFT_OF]);
        if (anchor != null) {
            childParams.mRight = anchor.mLeft - anchor.leftMargin - childParams.rightMargin;
        }
    }
}

注意:实际代码更复杂,上面是逻辑简化后的伪代码。

** 举个例子说明**

<View
    android:id="@+id/B"
    android:layout_toRightOf="@id/A"
    android:layout_alignParentRight="true"
    android:layout_marginLeft="8dp"
    android:layout_marginRight="8dp" />

调用 applyHorizontalSizeRules(…) 后会:

  1. 查找 A 的位置和宽度;
  2. 计算 B 的 mLeft = A.mRight + A.rightMargin + B.leftMargin;
  3. 同时根据 alignParentRight 计算 mRight = parentWidth - parentPaddingRight - B.rightMargin。

接下来会发生什么?

这些计算结果(mLeft 和 mRight)会被用于:

  • 计算 child 的可用宽度 → 用于 measureChildHorizontal();
  • 计算 child 的位置 → 用于 positionChildHorizontal();

** 结论**

方法 主要用途
applyHorizontalSizeRules() 解析横向布局依赖规则,更新 mLeft/mRight
measureChildHorizontal() 根据 mLeft/mRight 给 child 分配宽度并调用 measure()
positionChildHorizontal() 把 child 放到合适的 left/right 坐标上

mSortedHorizontalChildren

mSortedHorizontalChildren 是 RelativeLayout 中的一个内部成员变量,它用于存放按照“横向依赖关系”拓扑排序后的子 View 列表。它是 RelativeLayout 高性能支持 layout_toRightOf 等依赖规则的关键机制之一。

定义与类型

View[] mSortedHorizontalChildren;
  • 类型是 View[](数组);
  • 只包含 RelativeLayout 中参与水平方向定位的子 View;
  • 在 onMeasure() 阶段使用。

它为什么存在?

在 RelativeLayout 中,很多横向规则有依赖关系,例如:

<B android:layout_toRightOf="@id/A" />

表示 B 的位置依赖于 A 的右边界,所以我们 必须先处理 A,再处理 B。否则无法确定 B 的位置或宽度。

因此:

在遍历测量和布局子 View 前,RelativeLayout 需要先将子 View 排序,确保所有横向依赖的先后关系都能被正确满足。

排序机制:拓扑排序

Android 在内部通过拓扑排序算法,对子 View 的横向依赖进行排序:

mSortedHorizontalChildren = topologicalSort(children, HORIZONTAL);

这一步会:

  1. 找出每个子 View 的横向依赖(如 layout_toRightOf, alignLeft, alignRight 等);
  2. 构建一个“依赖图”;
  3. 通过拓扑排序,生成一个不违反依赖顺序的 View 列表;
  4. 存入 mSortedHorizontalChildren。

排序后的列表,满足以下条件:

  • 先处理 A,再处理依赖于 A 的 B
  • 防止在计算 B 的位置时,A 还未被测量或布局。

示例场景图示

依赖关系(横向):
C → B → A

结果:
mSortedHorizontalChildren = [A, B, C]

这样,处理顺序就不会出错。

作用总结

名称 含义
mSortedHorizontalChildren 横向排序后的子 View 数组
排序依据 横向依赖规则(如 toRightOf, alignLeft, alignParentRight)
排序目的 确保 measure/layout 顺序满足依赖
使用场景 onMeasure() 中先测宽度的阶段

测量宽高需要处理依赖关系

测量宽高为什么还有依赖关系?不是布局的时候才需要处理吗?

这是因为在 Android 中,宽高的计算(即 measure)本身就可能依赖其他 View 的位置或大小信息。这正是为什么在 RelativeLayout 中,测量和布局不可完全分离的根本原因。

我们先来明确下 Android 的三个阶段:

  1. measure → 确定每个 View 的大小(width 和 height);
  2. layout → 确定每个 View 的位置(left/top/right/bottom);
  3. draw → 将 View 绘制出来。

那为什么 measure 会依赖其它 View 呢?

情况一:子 View 的宽高是

wrap_content

如果一个子 View 的宽高是 wrap_content,那它需要:

  • 知道自己在布局中可用的空间
  • 才能根据内容和限制决定最终尺寸

在 RelativeLayout 中,“可用空间”经常取决于其它 View 的位置和大小

例如:

<View
    android:id="@+id/B"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_toRightOf="@id/A" />

这个 View B 想要知道它“还能多宽”,必须知道 A 的宽度和位置。

情况二:你使用了

alignParentRight+toLeftOf

<B
    android:layout_alignParentRight="true"
    android:layout_toLeftOf="@id/C"
    android:layout_width="match_parent" />

这个时候,B 的宽度等于 (父布局宽度 - C 的宽度 - margin),你必须先知道 C 的宽度,才能算出 B 的宽度。

举个实际图示(简化):

|----- RelativeLayout (宽度 300) -----|
|                                     |
|  A (宽 100)      B (wrap_content)   |
|_____________________________________|

B 是 layout_toRightOf=”@id/A”,且 layout_alignParentRight=”true”。

那 B 的最大宽度 = 300 - A 的宽度 = 200。

所以:必须先知道 A 的测量结果(宽度),才能计算 B 的宽度。

结论总结

阶段 是否可能有依赖
measure() 宽高 ✅ 需要依赖其他 View 的位置或大小
layout() 位置 ✅ 明显需要知道依赖 View 的位置

所以:measure 阶段不仅仅是“测大小”那么简单,它必须在特定顺序下被执行,才能得到正确结果。

为什么 RelativeLayout measure 两次

因为有时候:

  • 横向上的依赖 A → B;
  • 纵向上的依赖 B → A。

第一次测量时解决水平方向的宽度;

第二次再测量时解决垂直方向的高度。

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