美文网首页androidAndroid开发Android开发
RTFSC-RelativeLayout、LinearLayou

RTFSC-RelativeLayout、LinearLayou

作者: 二毛_coder | 来源:发表于2019-05-24 14:37 被阅读4次

    前言

    关于页面的性能如何优化,可能刚开始工作时,只知道减少层级或者使用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等也是一种优化手段。

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

    相关文章

      网友评论

        本文标题:RTFSC-RelativeLayout、LinearLayou

        本文链接:https://www.haomeiwen.com/subject/vrjgzqtx.html