本文是从网络上复制整理,方便个人阅读。正确性不置可否。
Android布局原则:
尽量多使用LinearLayout和RelativeLayout;FrameLayout使用在布局叠加的时;AbsoluteLayout已经废弃,不要使用;TableLayout已经被GridView替代,不建议使用。
在布局层次一样的情况下,建议使用LinearLayout代替RelativeLayout,因为LinearLayout性能要稍高一点。
将可复用的标签抽取出来并且通过include标签使用。
使用merge标签减少布局的嵌套层次。
使用ViewStub标签加载一些不常用的布局。
一、 RelativeLayout和LinearLayout是Android中常用的布局,两者的使用会极大的影响程序生成每一帧的性能,因此,正确的使用它们是提升程序性能的重要工作。下面将通过分析它们的源码来探讨其View绘制性能,并得出其正确的使用方法。
通过官方文档我们知道View的绘制进行measure, layout, draw,分别对应onMeasure(), onLayout, onDraw(),而他们的性能差异主要在onMeasure()上。
首先是RelativeLayout:
1 @Override
2 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
3 ......
4 View[] views = mSortedHorizontalChildren;
5 int count = views.length;
6
7 for (int i = 0; i < count; i++) {
8 View child = views[i];
9 if (child.getVisibility() != GONE) {
10 LayoutParams params = (LayoutParams) child.getLayoutParams();
11 int[] rules = params.getRules(layoutDirection);
12
13 applyHorizontalSizeRules(params, myWidth, rules);
14 measureChildHorizontal(child, params, myWidth, myHeight);
15
16 if (positionChildHorizontal(child, params, myWidth, isWrapContentWidth)) {
17 offsetHorizontalAxis = true;
18 }
19 }
20 }
21
22 views = mSortedVerticalChildren;
23 count = views.length;
24 final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
25
26 for (int i = 0; i < count; i++) {
27 View child = views[i];
28 if (child.getVisibility() != GONE) {
29 LayoutParams params = (LayoutParams) child.getLayoutParams();
30
31 applyVerticalSizeRules(params, myHeight);
32 measureChild(child, params, myWidth, myHeight);
33 if (positionChildVertical(child, params, myHeight, isWrapContentHeight)) {
34 offsetVerticalAxis = true;
35 }
36
37 if (isWrapContentWidth) {
38 if (isLayoutRtl()) {
39 if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
40 width = Math.max(width, myWidth - params.mLeft);
41 } else {
42 width = Math.max(width, myWidth - params.mLeft - params.leftMargin);
43 }
44 } else {
45 if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
46 width = Math.max(width, params.mRight);
47 } else {
48 width = Math.max(width, params.mRight + params.rightMargin);
49 }
50 }
51 }
52
53 if (isWrapContentHeight) {
54 if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
55 height = Math.max(height, params.mBottom);
56 } else {
57 height = Math.max(height, params.mBottom + params.bottomMargin);
58 }
59 }
60
61 if (child != ignore || verticalGravity) {
62 left = Math.min(left, params.mLeft - params.leftMargin);
63 top = Math.min(top, params.mTop - params.topMargin);
64 }
65
66 if (child != ignore || horizontalGravity) {
67 right = Math.max(right, params.mRight + params.rightMargin);
68 bottom = Math.max(bottom, params.mBottom + params.bottomMargin);
69 }
70 }
71 }
72 ......
73 }
根据上述关键代码,RelativeLayout分别对所有子View进行两次measure,横向纵向分别进行一次。
LinearLayout:
1 @Override
2 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
3 if (mOrientation == VERTICAL) {
4 measureVertical(widthMeasureSpec, heightMeasureSpec);
5 } else {
6 measureHorizontal(widthMeasureSpec, heightMeasureSpec);
7 }
8 }
根据线性布局方向,执行不同的方法,这里分析measureVertical方法。
1 void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
2 ......
3 for (int i = 0; i < count; ++i) {
4 ......
5
6 LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
7
8 totalWeight += lp.weight;
9
10 if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
11 // Optimization: don't bother measuring children who are going to use
12 // leftover space. These views will get measured again down below if
13 // there is any leftover space.
14 final int totalLength = mTotalLength;
15 mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
16 skippedMeasure = true;
17 } else {
18 int oldHeight = Integer.MIN_VALUE;
19
20 if (lp.height == 0 && lp.weight > 0) {
21 // heightMode is either UNSPECIFIED or AT_MOST, and this
22 // child wanted to stretch to fill available space.
23 // Translate that to WRAP_CONTENT so that it does not end up
24 // with a height of 0
25 oldHeight = 0;
26 lp.height = LayoutParams.WRAP_CONTENT;
27 }
28
29 // Determine how big this child would like to be. If this or
30 // previous children have given a weight, then we allow it to
31 // use all available space (and we will shrink things later
32 // if needed).
33 measureChildBeforeLayout(
34 child, i, widthMeasureSpec, 0, heightMeasureSpec,
35 totalWeight == 0 ? mTotalLength : 0);
36
37 if (oldHeight != Integer.MIN_VALUE) {
38 lp.height = oldHeight;
39 }
40
41 final int childHeight = child.getMeasuredHeight();
42 final int totalLength = mTotalLength;
43 mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
44 lp.bottomMargin + getNextLocationOffset(child));
45
46 if (useLargestChild) {
47 largestChildHeight = Math.max(childHeight, largestChildHeight);
48 }
49 }
50 ......
LinearLayout首先会对所有的子View进行measure,并计算totalWeight(所有子View的weight属性之和),然后判断子View的weight属性是否为最大,如为最大则将剩余的空间分配给它。如果不使用weight属性进行布局,则不进行第二次measure。
1 // Either expand children with weight to take up available space or
2 // shrink them if they extend beyond our current bounds. If we skipped
3 // measurement on any children, we need to measure them now.
4 int delta = heightSize - mTotalLength;
5 if (skippedMeasure || delta != 0 && totalWeight > 0.0f) {
6 float weightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;
7
8 mTotalLength = 0;
9
10 for (int i = 0; i < count; ++i) {
11 final View child = getVirtualChildAt(i);
12
13 if (child.getVisibility() == View.GONE) {
14 continue;
15 }
16
17 LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
18
19 float childExtra = lp.weight;
20 if (childExtra > 0) {
21 // Child said it could absorb extra space -- give him his share
22 int share = (int) (childExtra * delta / weightSum);
23 weightSum -= childExtra;
24 delta -= share;
25
26 final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
27 mPaddingLeft + mPaddingRight +
28 lp.leftMargin + lp.rightMargin, lp.width);
29
30 // TODO: Use a field like lp.isMeasured to figure out if this
31 // child has been previously measured
32 if ((lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)) {
33 // child was measured once already above...
34 // base new measurement on stored values
35 int childHeight = child.getMeasuredHeight() + share;
36 if (childHeight < 0) {
37 childHeight = 0;
38 }
39
40 child.measure(childWidthMeasureSpec,
41 MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));
42 } else {
43 // child was skipped in the loop above.
44 // Measure for this first time here
45 child.measure(childWidthMeasureSpec,
46 MeasureSpec.makeMeasureSpec(share > 0 ? share : 0,
47 MeasureSpec.EXACTLY));
48 }
49
50 // Child may now not fit in vertical dimension.
51 childState = combineMeasuredStates(childState, child.getMeasuredState()
52 & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
53 }
54
55 ......
56 }
57 ......
58 } else {
59 alternativeMaxWidth = Math.max(alternativeMaxWidth,
60 weightedMaxWidth);
61
62
63 // We have no limit, so make all weighted views as tall as the largest child.
64 // Children will have already been measured once.
65 if (useLargestChild && heightMode != MeasureSpec.EXACTLY) {
66 for (int i = 0; i < count; i++) {
67 final View child = getVirtualChildAt(i);
68
69 if (child == null || child.getVisibility() == View.GONE) {
70 continue;
71 }
72
73 final LinearLayout.LayoutParams lp =
74 (LinearLayout.LayoutParams) child.getLayoutParams();
75
76 float childExtra = lp.weight;
77 if (childExtra > 0) {
78 child.measure(
79 MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(),
80 MeasureSpec.EXACTLY),
81 MeasureSpec.makeMeasureSpec(largestChildHeight,
82 MeasureSpec.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时,例如一些网络异常的提示信息等。
亿速云「云服务器」,即开即用、新一代英特尔至强铂金CPU、三副本存储NVMe SSD云盘,价格低至29元/月。点击查看>>
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。