本文是从网络上复制整理,方便个人阅读。正确性不置可否。


Android布局原则:

尽量多使用LinearLayout和RelativeLayout;FrameLayout使用在布局叠加的时;AbsoluteLayout已经废弃,不要使用;TableLayout已经被GridView替代,不建议使用。

在布局层次一样的情况下,建议使用LinearLayout代替RelativeLayout,因为LinearLayout性能要稍高一点。

将可复用的标签抽取出来并且通过include标签使用。

使用merge标签减少布局的嵌套层次。

使用ViewStub标签加载一些不常用的布局。


一、 RelativeLayout和LinearLayout是Android中常用的布局,两者的使用会极大的影响程序生成每一帧的性能,因此,正确的使用它们是提升程序性能的重要工作。下面将通过分析它们的源码来探讨其View绘制性能,并得出其正确的使用方法。

RelativeLayout和LinearLayout是如何进行measure的?

通过官方文档我们知道View的绘制进行measure, layout, draw,分别对应onMeasure(), onLayout, onDraw(),而他们的性能差异主要在onMeasure()上。

首先是RelativeLayout:

1@Override2protectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec){3......4View[]views=mSortedHorizontalChildren;5intcount=views.length;67for(inti=0;i<count;i++){8Viewchild=views[i];9if(child.getVisibility()!=GONE){10LayoutParamsparams=(LayoutParams)child.getLayoutParams();11int[]rules=params.getRules(layoutDirection);1213applyHorizontalSizeRules(params,myWidth,rules);14measureChildHorizontal(child,params,myWidth,myHeight);1516if(positionChildHorizontal(child,params,myWidth,isWrapContentWidth)){17offsetHorizontalAxis=true;18}19}20}2122views=mSortedVerticalChildren;23count=views.length;24finalinttargetSdkVersion=getContext().getApplicationInfo().targetSdkVersion;2526for(inti=0;i<count;i++){27Viewchild=views[i];28if(child.getVisibility()!=GONE){29LayoutParamsparams=(LayoutParams)child.getLayoutParams();3031applyVerticalSizeRules(params,myHeight);32measureChild(child,params,myWidth,myHeight);33if(positionChildVertical(child,params,myHeight,isWrapContentHeight)){34offsetVerticalAxis=true;35}3637if(isWrapContentWidth){38if(isLayoutRtl()){39if(targetSdkVersion<Build.VERSION_CODES.KITKAT){40width=Math.max(width,myWidth-params.mLeft);41}else{42width=Math.max(width,myWidth-params.mLeft-params.leftMargin);43}44}else{45if(targetSdkVersion<Build.VERSION_CODES.KITKAT){46width=Math.max(width,params.mRight);47}else{48width=Math.max(width,params.mRight+params.rightMargin);49}50}51}5253if(isWrapContentHeight){54if(targetSdkVersion<Build.VERSION_CODES.KITKAT){55height=Math.max(height,params.mBottom);56}else{57height=Math.max(height,params.mBottom+params.bottomMargin);58}59}6061if(child!=ignore||verticalGravity){62left=Math.min(left,params.mLeft-params.leftMargin);63top=Math.min(top,params.mTop-params.topMargin);64}6566if(child!=ignore||horizontalGravity){67right=Math.max(right,params.mRight+params.rightMargin);68bottom=Math.max(bottom,params.mBottom+params.bottomMargin);69}70}71}72......73}


根据上述关键代码,RelativeLayout分别对所有子View进行两次measure,横向纵向分别进行一次。

LinearLayout:

1@Override2protectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec){3if(mOrientation==VERTICAL){4measureVertical(widthMeasureSpec,heightMeasureSpec);5}else{6measureHorizontal(widthMeasureSpec,heightMeasureSpec);7}8}

根据线性布局方向,执行不同的方法,这里分析measureVertical方法。

1voidmeasureVertical(intwidthMeasureSpec,intheightMeasureSpec){2......3for(inti=0;i<count;++i){4......56LinearLayout.LayoutParamslp=(LinearLayout.LayoutParams)child.getLayoutParams();78totalWeight+=lp.weight;910if(heightMode==MeasureSpec.EXACTLY&&lp.height==0&&lp.weight>0){11//Optimization:don'tbothermeasuringchildrenwhoaregoingtouse12//leftoverspace.Theseviewswillgetmeasuredagaindownbelowif13//thereisanyleftoverspace.14finalinttotalLength=mTotalLength;15mTotalLength=Math.max(totalLength,totalLength+lp.topMargin+lp.bottomMargin);16skippedMeasure=true;17}else{18intoldHeight=Integer.MIN_VALUE;1920if(lp.height==0&&lp.weight>0){21//heightModeiseitherUNSPECIFIEDorAT_MOST,andthis22//childwantedtostretchtofillavailablespace.23//TranslatethattoWRAP_CONTENTsothatitdoesnotendup24//withaheightof025oldHeight=0;26lp.height=LayoutParams.WRAP_CONTENT;27}2829//Determinehowbigthischildwouldliketobe.Ifthisor30//previouschildrenhavegivenaweight,thenweallowitto31//useallavailablespace(andwewillshrinkthingslater32//ifneeded).33measureChildBeforeLayout(34child,i,widthMeasureSpec,0,heightMeasureSpec,35totalWeight==0?mTotalLength:0);3637if(oldHeight!=Integer.MIN_VALUE){38lp.height=oldHeight;39}4041finalintchildHeight=child.getMeasuredHeight();42finalinttotalLength=mTotalLength;43mTotalLength=Math.max(totalLength,totalLength+childHeight+lp.topMargin+44lp.bottomMargin+getNextLocationOffset(child));4546if(useLargestChild){47largestChildHeight=Math.max(childHeight,largestChildHeight);48}49}50......

LinearLayout首先会对所有的子View进行measure,并计算totalWeight(所有子View的weight属性之和),然后判断子View的weight属性是否为最大,如为最大则将剩余的空间分配给它。如果不使用weight属性进行布局,则不进行第二次measure。

1//Eitherexpandchildrenwithweighttotakeupavailablespaceor2//shrinkthemiftheyextendbeyondourcurrentbounds.Ifweskipped3//measurementonanychildren,weneedtomeasurethemnow.4intdelta=heightSize-mTotalLength;5if(skippedMeasure||delta!=0&&totalWeight>0.0f){6floatweightSum=mWeightSum>0.0f?mWeightSum:totalWeight;78mTotalLength=0;910for(inti=0;i<count;++i){11finalViewchild=getVirtualChildAt(i);1213if(child.getVisibility()==View.GONE){14continue;15}1617LinearLayout.LayoutParamslp=(LinearLayout.LayoutParams)child.getLayoutParams();1819floatchildExtra=lp.weight;20if(childExtra>0){21//Childsaiditcouldabsorbextraspace--givehimhisshare22intshare=(int)(childExtra*delta/weightSum);23weightSum-=childExtra;24delta-=share;2526finalintchildWidthMeasureSpec=getChildMeasureSpec(widthMeasureSpec,27mPaddingLeft+mPaddingRight+28lp.leftMargin+lp.rightMargin,lp.width);2930//TODO:Useafieldlikelp.isMeasuredtofigureoutifthis31//childhasbeenpreviouslymeasured32if((lp.height!=0)||(heightMode!=MeasureSpec.EXACTLY)){33//childwasmeasuredoncealreadyabove...34//basenewmeasurementonstoredvalues35intchildHeight=child.getMeasuredHeight()+share;36if(childHeight<0){37childHeight=0;38}3940child.measure(childWidthMeasureSpec,41MeasureSpec.makeMeasureSpec(childHeight,MeasureSpec.EXACTLY));42}else{43//childwasskippedintheloopabove.44//Measureforthisfirsttimehere45child.measure(childWidthMeasureSpec,46MeasureSpec.makeMeasureSpec(share>0?share:0,47MeasureSpec.EXACTLY));48}4950//Childmaynownotfitinverticaldimension.51childState=combineMeasuredStates(childState,child.getMeasuredState()52&(MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));53}5455......56}57......58}else{59alternativeMaxWidth=Math.max(alternativeMaxWidth,60weightedMaxWidth);616263//Wehavenolimit,somakeallweightedviewsastallasthelargestchild.64//Childrenwillhavealreadybeenmeasuredonce.65if(useLargestChild&&heightMode!=MeasureSpec.EXACTLY){66for(inti=0;i<count;i++){67finalViewchild=getVirtualChildAt(i);6869if(child==null||child.getVisibility()==View.GONE){70continue;71}7273finalLinearLayout.LayoutParamslp=74(LinearLayout.LayoutParams)child.getLayoutParams();7576floatchildExtra=lp.weight;77if(childExtra>0){78child.measure(79MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(),80MeasureSpec.EXACTLY),81MeasureSpec.makeMeasureSpec(largestChildHeight,82MeasureSpec.EXACTLY));83}84}85}86}87......88}

提高绘制性能的使用方式

根据上面源码的分析,RelativeLayout将对所有的子View进行两次measure,而LinearLayout在使用weight属性进行布局时也会对子View进行两次measure,如果他们位于整个View树的顶端时并可能进行多层的嵌套时,位于底层的View将会进行大量的measure操作,大大降低程序性能。因此,应尽量将RelativeLayout和LinearLayout置于View树的底层,并减少嵌套。



二、Measure 和 Layout

从整体上来看 Measure 和 Layout 两个步骤的执行:
树的遍历是有序的,由父视图到子视图,每一个 ViewGroup 负责测绘它所有的子视图,而最底层的 View 会负责测绘自身。


具体分析


measure 过程由measure(int, int)方法发起,从上到下有序的测量 View ,在 measure 过程的最后,每个视图存储了自己的尺寸大小和测量规格。 layout 过程由layout(int, int, int, int)方法发起,也是自上而下进行遍历。在该过程中,每个父视图会根据 measure 过程得到的尺寸来摆放自己的子视图。

measure 过程会为一个View及所有子节点的 mMeasuredWidth 和 mMeasuredHeight 变量赋值,该值可以通过 getMeasuredWidth()getMeasuredHeight()方法获得。而且这两个值必须在父视图约束范围之内,这样才可以保证所有的父视图都接收所有子视图的测量。如果子视图对于 Measure 得到的大小不满意的时候,父视图会介入并设置测量规则进行第二次 measure。比如,父视图可以先根据未给定的 dimension 去测量每一个子视图,如果最终子视图的未约束尺寸太大或者太小的时候,父视图就会使用一个确切的大小再次对子视图进行 measure 。

measure 过程传递尺寸的两个类

ViewGroup.LayoutParams (View 自身的布局参数)

MeasureSpecs 类(父视图对子视图的测量要求)

ViewGroup.LayoutParams
这个类我们很常见,就是用来指定视图的高度和宽度等参数。对于每个视图的 height 和 width,你有以下选择:

MATCH_PARENT 表示子视图希望和父视图一样大(不包含padding值)

WRAP_CONTENT 表示视图为正好能包裹其内容大小(包含padding值)

ViewGroup 的子类有其对应的 ViewGroup.LayoutParams 的子类。比如 RelativeLayout 拥有的 ViewGroup.LayoutParams 的子类 RelativeLayoutParams。
有时我们需要使用 view.getLayoutParams() 方法获取一个视图 LayoutParams ,然后进行强转,但由于不知道其具体类型,可能会导致强转错误。其实该方法得到的就是其所在父视图类型的 LayoutParams,比如 View 的父控件为 RelativeLayout,那么得到的 LayoutParams 类型就为 RelativeLayoutParams。

MeasureSpecs
测量规格,包含测量要求和尺寸的信息,有三种模式:

UNSPECIFIED
父视图不对子视图有任何约束,它可以达到所期望的任意尺寸。比如ListView、ScrollView,一般自定义View中用不到,

EXACTLY
父视图为子视图指定一个确切的尺寸,而且无论子视图期望多大,它都必须在该指定大小的边界内,对应的属性为 match_parent 或具体指,比如 100dp,父控件可以通过MeasureSpec.getSize(measureSpec)直接得到子控件的尺寸。

AT_MOST
父视图为子视图指定一个最大尺寸。子视图必须确保它自己所有子视图可以适应在该尺寸范围内,对应的属性为 wrap_content,这种模式下,父控件无法确定子 View 的尺寸,只能由子控件自己根据需求去计算自己的尺寸,这种模式就是我们自定义视图需要实现测量逻辑的情况。


三、include

在实际开发中,我们经常会遇到一些共用的UI组件,比如带返回按钮的导航栏,如果为每一个xml文件都设置这部分布局,一是重复的工作量大,二是如果有变更,那么每一个xml文件都得修改。不过,我们可以将这些共用的组件抽取出来单独放到一个xml文件中,然后使用< include />标签导入到相应布局


四、merge

< merge />标签的作用是合并UI布局,使用该标签能降低UI布局的嵌套层次。该标签的主要使用场景主要包括两个,第一种情况是当xml文件的根布局是FrameLayout时,可以用merge作为根节点。理由是因为Activity的内容布局中,默认就用了一个FrameLayout作为xml布局根节点的父节点;第二种情况是当用include标签导入一个共用布局时,如果父布局和子布局根节点为同一类型,可以使用merge将子节点布局的内容合并包含到父布局中,这样就可以减少一级嵌套层次。这样就降低了布局嵌套层次。


五、ViewStub

ViewStub是Android布局优化中一个很不错的标签/控件,直接继承自View。但是真正用的可能不多。当对一个ViewStub调用inflate()方法或设置它可见时,系统会加载在ViewStub标签中引入的我们自己定义的View,然后填充在父布局当中。也就是说,在对ViewStub调用inflate()方法或设置visible之前,它是不占用布局空间和系统资源的。它的使用场景可以是在我们需要加载并显示一些不常用的View时,例如一些网络异常的提示信息等。