为什么 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(…) 后会:
- 查找 A 的位置和宽度;
- 计算 B 的 mLeft = A.mRight + A.rightMargin + B.leftMargin;
- 同时根据 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);
这一步会:
- 找出每个子 View 的横向依赖(如 layout_toRightOf, alignLeft, alignRight 等);
- 构建一个“依赖图”;
- 通过拓扑排序,生成一个不违反依赖顺序的 View 列表;
- 存入 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 的三个阶段:
- measure → 确定每个 View 的大小(width 和 height);
- layout → 确定每个 View 的位置(left/top/right/bottom);
- 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。
第一次测量时解决水平方向的宽度;
第二次再测量时解决垂直方向的高度。