Read The Fucking Source Code

The introduction

When choosing a layout, you must consider a comparative advantage of performance.

Android Q — API 29

1 Source code Analysis

1.1 onMeasure method

@Override protected void onMeasure(int widthMeasureSpec, Int heightMeasureSpec) {if (mOrientation == VERTICAL) {// MeasureMeasureVertical (widthMeasureSpec, heightMeasureSpec); } else {// measureHorizontal(widthMeasureSpec, heightMeasureSpec); }}Copy the code

1.2 measureVertical method

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);
        }
    }
Copy the code

1.3 the View resolveSizeAndState

This method gets the final view footprint of the subview

  • Size is the desired size of the subview.
  • SpecSize in measureSpec is the maximum size that a parent view can provide to a child view.
// The utility that coordinates the required size and state, and the constraints imposed by MeasureSpec. // The desired size will be used unless the constraint imposes a different size. // If the result is smaller than the desired size of the view, optionally set the bit 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) {// If the superview is the maximum mode, select the desired size and the minimum value of the maximum size given by the superview. case MeasureSpec.AT_MOST: if (specSize < size) { result = specSize | MEASURED_STATE_TOO_SMALL; } else { result = size; } break; // The size of the subview is the maximum size given by the superview. case MeasureSpec.EXACTLY: result = specSize; break; // If the parent view does not specify a mode, it is the desired size of the child view. case MeasureSpec.UNSPECIFIED: default: result = size; } return result | (childMeasuredState & MEASURED_STATE_MASK); }Copy the code

1.4 forceUniformWidth method

Private void forceUniformWidth(int count, int count) private void forceUniformWidth(int count, int count) Int heightMeasureSpec) {// Assume that the linear layout has the exact size. 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) {// Remeasure the subview with the measured height int oldHeight = lp.height; lp.height = child.getMeasuredHeight(); MeasureChildWithMargins (Child, uniformMeasureSpec, 0, heightMeasureSpec, 0); // Measure the sub view and restore the layout mode measureChildWithMargins(Child, uniformMeasureSpec, 0, heightMeasureSpec, 0); lp.height = oldHeight; }}}}Copy the code

1.5 Summary

  • LinearLayout performance: Broadly speaking, the LinearLayout performance is measured once without setting the weight attribute, and measured twice with the weight attribute.
  • The weight attribute is best implemented with the value 0dp in the direction. This is more in line with what you would expect: the concept of apportioned residuals and not showing if there is no residuals.
  • The worst scenario of the LinearLayout is measured three times, twice on the weight attribute direction and once on the non-weight attribute.

2 Test cases

Above the source code analysis, light from the code can also understand, but very obscure, not intuitive. Thinking that practice is the only criterion to test truth, if you can pass a very simple test force to verify the source logic. Isn’t it beautiful? A section of XML test case will be posted below, none of which need to be executed, just paste it into an XML and see it in action in the AS preview.

// The following test cases cover all the logic in the onMeasure method in the LinearLayout. <LinearLayout Android :id="@+ ID /test_linear_layout" Android :layout_width="wrap_content" The main purpose is to test the third measurement of the horizontal layout (parent imprecise mode + child match_parent property). Android :layout_height="wrap_content" // Can be set respectively: wrap_content/match_parent, analysis of source code effect difference. Android :orientation="vertical"> // //ChildView is a custom View inherited from the View (you can configure the log TAG, print the number of measurements). < com.kejiyuanren.layouttest.com mon ChildView / / this is non-standard usage of weight (not wrong, is can use, existence is reasonable). Android :id="@+ ID /linear_vertical_01" Android :layout_width="match_parent" Android :layout_height="wrap_content" Wrap_content / 0dp, analysis of source code effect differences. android:layout_weight="1" android:background="@android:color/holo_green_light" app:tag_name="linear_vertical_01" /> < com.kejiyuanren.layouttest.com mon ChildView / / common mode, do not have what effect, 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.com mon ChildView / / the view function, mainly to contrast effects of different parameters set the weight attribute. Android :id="@+ ID /linear_vertical_03" Android :layout_width="match_parent" Android :layout_height="wrap_content" Wrap_content/match_parent have the same effect, so it's easier to understand how the measurement model treats both of them similarly. Android :visibility="gone" // You can set the value to "gone"/" visible "to understand the difference in weight parameters. android:background="@android:color/holo_blue_dark" app:tag_name="linear_vertical_03" /> < com.kejiyuanren.layouttest.com mon ChildView / / this is standard usage of weight. Android :id="@+ ID /linear_vertical_04" Android :layout_width="match_parent" Android :layout_height="0dp" Wrap_content / 0dp, analysis of source code effect differences. android:layout_weight="2" android:background="@android:color/black" app:tag_name="linear_vertical_04" /> </LinearLayout>Copy the code

To get started, don’t bother: Ctrl+C -> Ctrl+ V -> XML file -> AS preview to see how it looks. If you understand all the effects of the test case, then the LinearLayout measurement method is basically mastered.

3 Problem Thinking

Is the weight property used to divide up the remaining space?

  • The weight attribute is used to balance the final computation space.
  • What is equilibrium? It might zoom in, it might zoom out. Why is that?
  • Magnification is easy to understand. For example, if there is space left over, balance the remaining space to the View with the weight attribute.
  • For example, if the calculated space is three times the size of the parent container, then the view corresponding to weight needs to be reduced in equal proportion (the child view without the weight attribute is not affected). Only part of the child view may be displayed in the end, and some child views will not be displayed if they are not in the parent container at all.

Isn’t the LinearLayout only measured twice at most? How can you measure it three times?

  • Let’s talk about longitudinal measurements.
  • The vertical LinearLayout sets the weight property and measures it twice.
  • But what everyone ignores is the special case of landscape: the parent’s width is not EXACTLY mode and the child’s width is the match_parent property.
  • If there is a special horizontal scenario, the forceUniformWidth method of the LinearLayout is called to walk through all the subviews to measure again.
  • So, if the above scenarios exist at the same time, three measurements will be made: two vertically + one horizontally.
  • Personally, I understand that the lateral measurement scenario (third measurement) mentioned above should be avoided as much as possible in development. For example, if the parent view uses the wrap_content property, the child view uses the wrap_content property as well as the match_parent property. (Personal view, if there are mistakes, please correct.)

Xiaobian extension links

Android View Module Family Bucket