Read The Fucking Source Code
引言
选择布局肯定要考虑性能的优略对比。
源码版本(Android Q — API 29)
1 源码分析
1.1 onMeasure 方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
//纵向测量
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
//横向测量
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
1.2 纵向 measureVertical 方法
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
mTotalLength = 0;
int maxWidth = 0;
int childState = 0;
int alternativeMaxWidth = 0;
int weightedMaxWidth = 0;
boolean allFillParent = true;
float totalWeight = 0;
//获取子视图的个数
final int count = getVirtualChildCount();
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
boolean matchWidth = false;
boolean skippedMeasure = false;
final int baselineChildIndex = mBaselineAlignedChildIndex;
final boolean useLargestChild = mUseLargestChild;
int largestChildHeight = Integer.MIN_VALUE;
int consumedExcessSpace = 0;
int nonSkippedChildCount = 0;
// 遍历每个视图的高度,并且记录最大宽度
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
//如果子视图为空,那么高度为0(measureNullChild的返回是0)
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
//如果子视图为空,那么计算跳过多少子View的测量过程,默认是0(getChildrenSkipCount返回0)
if (child.getVisibility() == View.GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
//需要测量的子视图个数
nonSkippedChildCount++;
//确定子视图前面是否有分割线
if (hasDividerBeforeChildAt(i)) {
mTotalLength += mDividerHeight;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//整体高度是子视图高度的叠加
totalWeight += lp.weight;
//将剩余空间进行均等化分配(说人话:设置了常规模式的weight(height为0 & weight属性设置为大于0的值))
final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
//父视图的测量模式是精准模式
if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
final int totalLength = mTotalLength;
// 当在weight模式时,子视图高度暂时不计算,后续会针对weight进行二次测量。
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
//设置标记位,后续会用到。
skippedMeasure = true;
} else {
if (useExcessSpace) {
//父视图不是精准模式,子视图且设置了常规模式的weight,暂时设置子视图的高度为wrap_content,方便测量,之后会进行恢复。
lp.height = LayoutParams.WRAP_CONTENT;
}
//确定这个孩子想要多大。
//如果这个或以前的孩子已经给了一个权重,那么我们允许它使用所有可用的空间(如果需要的话,我们会在以后缩小东西)。
final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
//进行一次策略,获得子视图对应的MeasureSpec
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
final int childHeight = child.getMeasuredHeight();
if (useExcessSpace) {
//设置了常规模式的weight,恢复原来的高度,并记录我们分配给weight属性的空间,以便我们能够精确匹配测量行为。
lp.height = 0;
consumedExcessSpace += childHeight;
}
//计算总高度
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
//最大子视图模式,xml中配置,不常用,默认false,忽略即可。
if (useLargestChild) {
largestChildHeight = Math.max(childHeight, largestChildHeight);
}
}
//baseline的设置,不做展开。
if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
mBaselineChildTop = mTotalLength;
}
// baseline和weight的互斥场景,下面的异常信息已经说的很明白了。
if (i < baselineChildIndex && lp.weight > 0) {
throw new RuntimeException("A child of LinearLayout with index "
+ "less than mBaselineAlignedChildIndex has weight > 0, which "
+ "won't work. Either remove the weight, or don't set "
+ "mBaselineAlignedChildIndex.");
}
boolean matchWidthLocally = false;
if (widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT) {
//线性布局的宽度将按比例缩放,至少有一个孩子说它想与我们的宽度匹配。
//设置一个标志,表明当我们知道宽度时,至少需要重新测量该视图。
matchWidth = true;
matchWidthLocally = true;
}
//计算最大宽度,并融合测量状态
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
childState = combineMeasuredStates(childState, child.getMeasuredState());
//设置需要填充父容器的标志位
allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
if (lp.weight > 0) {
//如果我们最终重新测量,加权视图的宽度是假的,所以将它们分开。
weightedMaxWidth = Math.max(weightedMaxWidth,
matchWidthLocally ? margin : measuredWidth);
} else {
alternativeMaxWidth = Math.max(alternativeMaxWidth,
matchWidthLocally ? margin : measuredWidth);
}
//逻辑同上,忽略。
i += getChildrenSkipCount(child, i);
}
//存在需要测量的子视图,并且最后一个子视图存在分割线,则需要叠加分割线的高度。
if (nonSkippedChildCount > 0 && hasDividerBeforeChildAt(count)) {
mTotalLength += mDividerHeight;
}
//最大子视图模式(默认false,可忽略),校正特殊模式下的最大高度。
if (useLargestChild &&
(heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED)) {
//代码忽略……
}
}
// 添加根视图的padding,因为接下来要计算父视图的剩余空间
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
// 获取最大期望高度
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
// 使我们计算的尺寸与高度相一致(resolveSizeAndState对三个模式进行区分,逻辑简单,不作详细说明)
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
// 存在剩余空间或者存在超出空间,进行放大或者缩小的处理。
int remainingExcess = heightSize - mTotalLength
+ (mAllowInconsistentMeasurement ? 0 : consumedExcessSpace);
//满足重新测量的条件(肯定有weight属性的设置)
//根布局是精准模式:skippedMeasure。
//根布局是非精准模式:其他一大坨条件(可以说是肯定为true,因为sRemeasureWeightedChildren一直为true,大家可以源码追溯)。
if (skippedMeasure
|| ((sRemeasureWeightedChildren || remainingExcess != 0) && totalWeight > 0.0f)) {
//weight的总值计算,可以看出,weightSum可以是缺省值。
float remainingWeightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;
mTotalLength = 0;
//进行第二次测量
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
//子视图为空或者为GONE,忽略即可。
if (child == null || child.getVisibility() == View.GONE) {
continue;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final float childWeight = lp.weight;
//存在weight设置且合理,则进行二次测量
if (childWeight > 0) {
//按比例根据剩余空间进行计算并更新整体剩余空间等信息。
final int share = (int) (childWeight * remainingExcess / remainingWeightSum);
remainingExcess -= share;
remainingWeightSum -= childWeight;
final int childHeight;
if (mUseLargestChild && heightMode != MeasureSpec.EXACTLY) {
//最大子视图模式,忽略
childHeight = largestChildHeight;
} else if (lp.height == 0 && (!mAllowInconsistentMeasurement
|| heightMode == MeasureSpec.EXACTLY)) {
//常规weight设置情况下,进行零开始布置,只使用它那部分多余的空间。
childHeight = share;
} else {
// 其他情况下的weight设置场景,有一些固有的高度,我们需要增加他多余的空间。
childHeight = child.getMeasuredHeight() + share;
}
//纵向根据二次计算的值,设置为子视图为精准模式
final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
Math.max(0, childHeight), MeasureSpec.EXACTLY);
//横向则常规测量方式
final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin,
lp.width);
//进行子视图测量调用
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
// 测量状态融合.
childState = combineMeasuredStates(childState, child.getMeasuredState()
& (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
}
//计算并记录最大子视图的宽度
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
//横向:设置标记为:根视图不是精准模式 & 子视图是match_parent模式。
//秉承一贯的计算逻辑,和上面的第一次测量的计算逻辑一致(每次测量都会对宽度和高度信息进行记录更新,万变不离其宗)。
boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY &&
lp.width == LayoutParams.MATCH_PARENT;
alternativeMaxWidth = Math.max(alternativeMaxWidth,
matchWidthLocally ? margin : measuredWidth);
allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
//更新最大高度信息
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() +
lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
}
// 添加padding
mTotalLength += mPaddingTop + mPaddingBottom;
// TODO: Should we recompute the heightSpec based on the new total length?
} else {
//不进行二次测量的场景
//最大宽度获取
alternativeMaxWidth = Math.max(alternativeMaxWidth,
weightedMaxWidth);
// 最大子视图模式:我们没有限制,所以让所有加权视图都和最大的孩子一样高。孩子们已经测量过一次了。
if (useLargestChild && heightMode != MeasureSpec.EXACTLY) {
//代码忽略……
}
//最大宽度的计算设置
if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
maxWidth = alternativeMaxWidth;
}
maxWidth += mPaddingLeft + mPaddingRight;
// Check against our minimum width
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
if (matchWidth) {
//强制均衡高度。
forceUniformWidth(count, heightMeasureSpec);
}
}
1.3 View resolveSizeAndState
该方法是获取子视图的最终视图占用大小
- size 是子视图的期望尺寸大小。
- measureSpec 中的 specSize 是父视图能提供给子视图的最大尺寸。
//协调所需大小和状态的实用程序,以及由MeasureSpec施加的约束。
//将采用所需的大小,除非约束施加了不同的大小。
//返回值是一个复合整数,解析的大小位于MEASURED_SIZE_MASK的size位,
//如果结果大小小于视图想要的大小,则可以选择设置位MEASURED_STATE_TOO_SMALL。
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
//父视图是最大模式,那么选择 期望尺寸 和 父视图给定最大尺寸 的最小值。
case MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
//父视图是精准模式,那么子视图的尺寸就是父视图给定最大尺寸。
case MeasureSpec.EXACTLY:
result = specSize;
break;
//父视图是未指定模式,则为子视图期望的尺寸大小。
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
1.4 forceUniformWidth 方法
//当根视图的横向测量模式是最大模式 & 子视图的横向宽度设置为match_parent时
//需要强制均衡宽度
private void forceUniformWidth(int count, int heightMeasureSpec) {
// 假设线性布局具有精确的大小。
int uniformMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(),
MeasureSpec.EXACTLY);
for (int i = 0; i< count; ++i) {
final View child = getVirtualChildAt(i);
if (child != null && child.getVisibility() != GONE) {
LinearLayout.LayoutParams lp = ((LinearLayout.LayoutParams)child.getLayoutParams());
if (lp.width == LayoutParams.MATCH_PARENT) {
// 使用已经测量过的高度进行子视图的重新测量
int oldHeight = lp.height;
lp.height = child.getMeasuredHeight();
// 测量子视图,并恢复子视图高度的布局模式
measureChildWithMargins(child, uniformMeasureSpec, 0, heightMeasureSpec, 0);
lp.height = oldHeight;
}
}
}
}
1.5 汇总小结
- LinearLayout 的布局性能:广义上可以说,不设置 weight 属性,测量一次,设置了 weight 属性,测量两次。
- weight 属性最好搭配所在方向上的数值 0dp 来实现。这样更符合大家所预期的:分摊剩余的概念,如果没有剩余则不显示。
- LinearLayout 最坏的场景时测量三次,weight 属性方向上测量两次 + 非 weight 属性上测量一次。
2 测试用例
上面的源码分析,光从代码看也能看懂,但是很晦涩,不直观。 想着实践才是检验真理的唯一标准,如果能通过一个很简单的测试用力来验证下源码逻辑。岂不美哉? 下面会贴出一段 xml 的测试用例,都不用执行,只要粘贴到一个 xml 中,在 AS 预览中就可以看到实际效果。
//下面的测试用例,涵盖了LinearLayout中onMeasure(纵向)测量方法中的全部逻辑。
<LinearLayout
android:id="@+id/test_linear_layout"
android:layout_width="wrap_content" //这么设置,主要是为了测试横向布局的第三次测量(父视图非精准模式 + 子视图match_parent属性)。
android:layout_height="wrap_content" //可分别设置:wrap_content / match_parent,分析源码效果差异。
android:orientation="vertical"> //纵向,契合上面的纵向源码分析。
//ChildView :其实就是个继承自View的自定义View(可以配置log的TAG,打印测量次数)。
<com.kejiyuanren.layouttest.common.ChildView //这个是weight的非标准用法(不是不对,是可以用的,存在即合理)。
android:id="@+id/linear_vertical_01"
android:layout_width="match_parent"
android:layout_height="wrap_content" //可以分别设置:wrap_content / 0dp,分析源码效果差异。
android:layout_weight="1"
android:background="@android:color/holo_green_light"
app:tag_name="linear_vertical_01" />
<com.kejiyuanren.layouttest.common.ChildView //常用模式,没啥作用,起到分割醒目的作用
android:id="@+id/linear_vertical_02"
android:layout_width="match_parent"
android:layout_height="20dp"
android:background="@android:color/holo_red_dark"
app:tag_name="linear_vertical_02" />
<com.kejiyuanren.layouttest.common.ChildView //这个视图的作用,主要是为了对比设置了weight属性的不同参数效果。
android:id="@+id/linear_vertical_03"
android:layout_width="match_parent"
android:layout_height="wrap_content" //可分别设置:wrap_content / match_parent,效果一样,可以更好理解测量模式对两者的类似处理。
android:visibility="gone" // 可分别设置:gone / visible,理解weight属性的差异参数对比。
android:background="@android:color/holo_blue_dark"
app:tag_name="linear_vertical_03" />
<com.kejiyuanren.layouttest.common.ChildView //这个是weight的标准用法。
android:id="@+id/linear_vertical_04"
android:layout_width="match_parent"
android:layout_height="0dp" //可以分别设置:wrap_content / 0dp,分析源码效果差异。
android:layout_weight="2"
android:background="@android:color/black"
app:tag_name="linear_vertical_04" />
</LinearLayout>
动起手来,不要嫌麻烦:Ctrl+C -> Ctrl + V -> xml 文件中 -> AS 预览看效果即可。 如果大家理解了测试用例中所有效果,那么对 LinearLayout 的测量方法,就基本掌握了。
3 问题思考
设置了 weight 属性,是用来瓜分剩余空间的吗?
- 这个说法也对也不对,其实更准确的说法是:设置了 weight 属性,是用来平衡最后的计算空间。
- 平衡怎么理解呢?就是可能放大,可能缩小。为什么呢?
- 放大很好理解了,比如空间有剩余,则将剩余空间平衡给设置了 weight 属性的 View。
- 缩小的话:比如计算出的空间是父容器三倍大小,那么就需要将 weight 对应的视图进行等比例的缩小(没有weight 属性的子视图不受影响),可能最终只能显示一部分子视图,有些子视图完全不在父容器中,则不会显示。
LinearLayout 不是最多只测量两次吗?怎么会测量三次?
- 我们就拿纵向上的测量方法进行讨论。
- 大家平常说的测量两次,是说的纵向的 LinearLayout 设置了 weight 属性,会测量两次。
- 但是大家忽略了横向的特殊场景:父视图宽度的测量模式不是精准模式(EXACTLY)+ 子视图的宽度是 match_parent 属性。
- 如果存在这种横向的特殊场景,那么就会调用 LinearLayout 的 forceUniformWidth 方法,进行遍历所有子视图进行再次测量。
- 所以,如果上面的场景同时存在,则会测量三次:两次纵向 + 一次横向。
- 个人理解上面所说的横向测量场景(第三次测量),要在开发中尽可能的取去规避。比如父视图在使用了wrap_content属性时,子视图尽量用wrap_content属性(和用match_parent属性)效果一致。(个人观点,如有错误,请指正。)
小编的扩展链接
今天的文章Android 布局性能 LinearLayout分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/15353.html