androidAndroid开发Android开发

RTFSC-RelativeLayout、LinearLayou

2019-05-24  本文已影响4人  二毛_coder

前言

关于页面的性能如何优化,可能刚开始工作时,只知道减少层级或者使用ViewStub懒加载控件等方式来优化。如果要做更加深入的优化怎么办呢?(层级已经减少到尽量最低了,在初始化不需要显示的布局也使用了懒加载的方式了),那么我们可能需要具体去对每一个View的使用进行深度的分析了!
来看一个现象:用Android studio,新建一个Activity自动生成的布局文件之前都是RelativeLayout(当然现在默认是ConstraintLayout),当然这是SDK有意为之的。为什么呢?当然是这样性能更好咯,性能至上嘛。但是当我们去查看content的顶级View,也就是DecoreView时,发现它的内部是一个LinearLayout,上面是标题栏,下面是内容栏。那么问题来了,Google为什么给开发者默认新建了个RelativeLayout/ConstraintLayout,而自己却偷偷用LinearLayout,到底谁的性能更高呢?所以引出了本篇对RelativeLayout、LinearLayout、FrameLayout的性能分析。

image.png

分析之前需要知道一些View的基本问题。

View是什么?

View是Android系统在屏幕上的呈现的一种表现形式,也就是说你在屏幕上能看到的东西都是View。

View是怎么绘制出来的?

View的绘制流程是从ViewRootImpl的performTraversals()方法开始,依次经过measure(),layout()和draw()三个过程,各自方法进行深度遍历。才最终将一个View绘制出来。

View是怎么呈现在界面上的?

Android中的视图都是通过Window来呈现的,不管Activity、Dialog还是Toast它们都有一个Window,然后通过WindowManager来管理View。Window和顶级View(DecorView)的通信是依赖ViewRootImpl完成的。

View和ViewGroup什么区别?

不管简单的Button和TextView还是复杂的RelativeLayout和ListView,他们的共同基类都是View。所以说,View是一种界面层控件的抽象,他代表了一个控件。那ViewGroup是什么东西,它可以被翻译成控件组,即一组View。ViewGroup也是继承View,这就意味着View本身可以是单个控件,也可以是多个控件组成的控件组。根据这个理论,Button显然是个View,而RelativeLayout不但是一个View还可以是一个ViewGroup,而ViewGroup内部是可以有子View的,这个子View同样也可能是ViewGroup,以此类推。

RelativeLayout与LinearLayout性能PK

当RelativeLayout和LinearLayout分别作为ViewGroup,表达相同布局时绘制在屏幕上时谁更快一点。上面已经简单说了View的绘制,从ViewRoot的performTraversals()方法开始依次调用perfromMeasure、performLayout和performDraw这三个方法。这三个方法分别完成顶级View的measure、layout和draw三大流程,其中perfromMeasure会调用measure,measure又会调用onMeasure,在onMeasure方法中则会对所有子元素进行measure,这个时候measure流程就从父容器传递到子元素中了,这样就完成了一次measure过程,接着子元素会重复父容器的measure,如此反复就完成了整个View树的遍历。同理,performLayout和performDraw也分别完成perfromMeasure类似的流程。通过这三大流程,分别遍历整棵View树,就实现了Measure,Layout,Draw这一过程,View就绘制出来了。所以接下来我们可以跟踪一下RelativeLayout和LinearLayout这三大流程的执行耗时。我们可以构建一个相同的布局,来比对谁更耗时:


image.png

分别用LinearLayout和RelativeLayout来作为父布局。

public class MyRelativeLayout extends RelativeLayout {
    private static final String TAG = "MyRelativeLayout";

    public MyRelativeLayout(Context context) {
        super(context);
        setWillNotDraw(false);
    }

    public MyRelativeLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        setWillNotDraw(false);
    }

    public MyRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setWillNotDraw(false);
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        long s = System.nanoTime();
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        Log.i(TAG, "onMeasure time: "+(System.nanoTime() - s));
        Log.i(TAG, "onMeasure: ");
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        long s = System.nanoTime();
        super.onLayout(changed, l, t, r, b);
        Log.i(TAG, "onLayout time: "+(System.nanoTime() - s));
        Log.i(TAG, "onLayout: ");
    }

    @Override
    protected void onDraw(Canvas canvas) {
        long s = System.nanoTime();
        super.onDraw(canvas);
        Log.i(TAG, "onDraw time: "+(System.nanoTime() - s));
        Log.i(TAG, "onDraw: ");
    }
}

LinearLayout的代码也是同理。
LinearLayout

Measure:4617769 (4.617ms)
Layout:1005308 (1.005ms)
draw:56461 (0.056ms)

RelativeLayout

Measure:1947154 (1.947ms)
Layout:993924 (0.994ms)
draw:62923 (0.063ms)
从打印的结果来看,无论使用RelativeLayout还是LinearLayout,layout和draw的过程两者相差无几,考虑到误差的问题,可以认为两者耗时相同,关键是Measure的过程RelativeLayout却比LinearLayout慢了一大截。

所以,这里我们可以分析一下Measure里面到底干了什么?

RelativeLayout的onMeasure()方法
 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    ...

        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;
                }
            }
        }

   ...

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

                applyVerticalSizeRules(params, myHeight, child.getBaseline());
                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);
                }
            }
        }

    ...

        setMeasuredDimension(width, height);
    }

根据源码我们发现RelativeLayout会对子View做两次measure。why?首先RelativeLayout中子View的排列方式是基于彼此的依赖关系,而这个依赖关系可能和布局中View的顺序并不相同,在确定每个子View的位置的时候,就需要先给所有的子View排序一下。又因为RelativeLayout允许A,B 两个子View,横向上B依赖A,纵向上A依赖B。所以需要横向纵向分别进行一次排序测量。

LinearLayout的onMeasure()方法
   @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mOrientation == VERTICAL) {
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        }
    }

LinearLayout的onMeasure比较简单,先判断orientation,排列方向,然后根据对应方向完成一次测量即可!

 void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
      ...
        for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                mTotalLength += measureNullChild(i);
                continue;
            }

            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;

            final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
            if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
                skippedMeasure = true;
            } else {
                if (useExcessSpace) {
                    lp.height = LayoutParams.WRAP_CONTENT;
                }

                final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
                measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                        heightMeasureSpec, usedHeight);

                final int childHeight = child.getMeasuredHeight();
                if (useExcessSpace) {
                    lp.height = 0;
                    consumedExcessSpace += childHeight;
                }

                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                       lp.bottomMargin + getNextLocationOffset(child));

                if (useLargestChild) {
                    largestChildHeight = Math.max(childHeight, largestChildHeight);
                }
            }
        ...
        if (skippedMeasure
                || ((sRemeasureWeightedChildren || remainingExcess != 0) && totalWeight > 0.0f)) {
            float remainingWeightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;

            mTotalLength = 0;

            for (int i = 0; i < count; ++i) {
                final View child = getVirtualChildAt(i);
                if (child == null || child.getVisibility() == View.GONE) {
                    continue;
                }

                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                final float childWeight = lp.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)) {
                        childHeight = share;
                    } else {
                        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);

                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));
            }

            // Add in our padding
            mTotalLength += mPaddingTop + mPaddingBottom;
            // TODO: Should we recompute the heightSpec based on the new total length?
        }
        ...
    }

LinearLayout在对子View进行measure操作的过程中,使用变量mTotalLength保存已经测量过的child所占用的高度,该变量默认是0。在for循环中调用measureChildBeforeLayout方法对每一个child进行测量。 每次for循环对child测量完毕后,调用child.getMeasuredHeight()获取该子视图最终的高度,并将这个高度添加到mTotalLength中。接下来处理lp.weight>0的情况需要注意,如果变量heightMode是EXACTLY,那么,当其他子视图占满父视图的高度后,weight>0的子视图分配不到布局空间,不被显示,只有当heightMode是AT_MOST或UNSPECIFIED时,weight>0的视图才能优先获得布局高度。
总结:如果不使用weight属性,LinearLayout会在mOrientation 指定的方向上进行一次measure的过程,如果使用weight属性,LinearLayout会先过滤设置过weight属性的view做一次measure,之后再对设置过weight属性的view做一次measure。由此可见,weight属性对性能是有一定的影响,

总结RelativeLayout和LinearLayout:之前测试的数据结果之所以RelativeLayout的measure耗时会是LinearLayout的两倍左右的原因是,RelativeLayout会对子view进行两次measure。而LinearLayout只需要一次。但是LinearLayout中如果存在layout_weight属性,也会有第二次view的测量,但是性能仍会比RelativeLayout好。

FrameLayout与LinearLayout性能PK

这次布局中间只放一个居中的view,代码较简单,就不贴出来了。
LinearLayout

Measure:356923(0.357ms)
Layout:399307(0.399ms)
draw:56153 (0.056ms)

FrameLayout

Measure:285231(0.285ms)
Layout:349308(0.349ms)
draw:53528 (0.053ms)
似乎没有数据较大的过程

FrameLayout的onMeasure()方法
  @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
       ....

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }

       ...
        count = mMatchParentChildren.size();
        if (count > 1) {
            for (int i = 0; i < count; i++) {
                final View child = mMatchParentChildren.get(i);
                final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

                final int childWidthMeasureSpec;
                if (lp.width == LayoutParams.MATCH_PARENT) {
                    final int width = Math.max(0, getMeasuredWidth()
                            - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                            - lp.leftMargin - lp.rightMargin);
                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                            width, MeasureSpec.EXACTLY);
                } else {
                    childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                            lp.leftMargin + lp.rightMargin,
                            lp.width);
                }

                final int childHeightMeasureSpec;
                if (lp.height == LayoutParams.MATCH_PARENT) {
                    final int height = Math.max(0, getMeasuredHeight()
                            - getPaddingTopWithForeground() - getPaddingBottomWithForeground()
                            - lp.topMargin - lp.bottomMargin);
                    childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                            height, MeasureSpec.EXACTLY);
                } else {
                    childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                            getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
                            lp.topMargin + lp.bottomMargin,
                            lp.height);
                }

                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }
    }

代码中可以看到会对各个子view进行一次measure,如果FrameLayout宽或高是EXACTLY 模式,子view又是MATCH_PARENT模式的话,会进行第二次measure。这里我设置的布局不存在这样的情况。猜想如果给FrameLayout设置一个固定宽高,子view设置MATCH_PARENT的话,measure耗时会增加~

结论

1.RelativeLayout会让子View调用两次onMeasure,LinearLayout 在有weight时,也会调用子View两次onMeasure
2.在不影响层级深度的情况下,使用LinearLayout和FrameLayout而不是RelativeLayout。
3.能用两层LinearLayout,尽量用一个RelativeLayout,在时间上此时RelativeLayout耗时更小。另外LinearLayout慎用layout_weight,否则会增加耗时。总之减少层级结构,才是王道,让onMeasure做延迟加载,用viewStub,include等也是一种优化手段。

个人理解,如有错误,欢迎指出~

上一篇下一篇

猜你喜欢

热点阅读