Android 布局性能 LinearLayout

Android 布局性能 LinearLayoutRead The Fucking Source Code 视图说明:也就是用户看到的页面,也就是View相关

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 视图模块 全家桶》

今天的文章Android 布局性能 LinearLayout分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/15353.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注