【UI布局优化】Android布局优化的几种方式

在Android中,布局优化越来越受到重视,下面将介绍布局优化的几种方式,这几种方式一般可能都见过,因为现在用的还比较多,我们主要从两个方面来进行介绍,一方面是用法,另一方面是从源码来分析,为什么它能起到优化的效果。

一、几种方式的用法
1、布局重用

这个标签的主要作用就是它能够重用布局文件,如果一些布局在许多布局文件中都需要被使用,我们就可以把它单独写在一个布局中,然后使用这个标签在需要使用它的地方把这个布局加进去,这样就达到了重用的目的,最典型的一个用法就是,如果我们自定义了一个TitleBar,这个TitleBar可能需要在每个Activity的布局文件中都使用到,这样我们就可以使用这个标签来实现,下面来举个例子。

"http://schemas.android.com/apk/res/android"  
    android:orientation="vertical"   
    android:layout_width=”match_parent”  
    android:layout_height=”match_parent”  
    android:background="@color/app_bg"  
    android:gravity="center_horizontal">  

    "@+id/titlebar"
              layout="@layout/titlebar"/>  

    "wrap_content"  
              android:text="@string/hello"  
              android:padding="10dp" />  

    ...  

  

上面就代表一个Activity的布局文件,我们自己写了一个titleBar布局,直接使用inclue标签的layout来指定就可以把这个titleBar的布局文件加入进去,这样在每个Activity中我们就可以使用include标签来重用这个titleBar布局了,不需要在每个里面都重复写一个titleBar的布局了,下面我们来看看这个titleBar的布局文件。


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="65dp"
    android:gravity="center">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="首页"/>

LinearLayout>

上面只是我们简单的写了一个titleBar的布局文件,我们可以根据需要自己来写一个。

在代码中,如果我们希望得到这个titlebar的View,我们只需要跟其他控件一样,使用findViewById来得到这个titleBar布局的View并且可以对其进行相应的操作。

总结一点:这个标签主要是做到布局的重用,使用这个标签可以把公共布局嵌入到所需要嵌入的地方。

2、减少视图层级

这个标签的作用就是删减多余的层级,优化UI,具体什么意思呢?还是来是例子来说明,下面我们来看一个布局文件。

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">

    <ImageView  
        android:layout_width="fill_parent" 
        android:layout_height="fill_parent" 

        android:scaleType="center"
        android:src="@drawable/golden_gate" />

    <TextView
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" 
        android:layout_marginBottom="20dip"
        android:layout_gravity="center_horizontal|bottom"

        android:padding="12dip"

        android:background="#AA000000"
        android:textColor="#ffffffff"

        android:text="Golden Gate" />

FrameLayout>

这个布局文件比较简单,就是一个FrameLayout里面放了一个ImageView和一个TextView。下面我们来使用HierarchyViewer来查看它的布局层次。

【UI布局优化】Android布局优化的几种方式_第1张图片

从这个布局层次,就可以看到我们的FrameLayout的父布局仍然是一个FrameLayout,其实它们是重复的,我们其实不需要使用一个FrameLayout,而是直接将我们的内容挂载上层的那个FrameLayout下面就可以,这样怎么做呢?使用merge标签就可以了,我们使用merge就代表merge里面的内容的父布局就是merge这个标签的父布局,这样就重用了父布局。

<merge xmlns:android="http://schemas.android.com/apk/res/android">

    <ImageView  
        android:layout_width="fill_parent" 
        android:layout_height="fill_parent" 

        android:scaleType="center"
        android:src="@drawable/golden_gate" />

    <TextView
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" 
        android:layout_marginBottom="20dip"
        android:layout_gravity="center_horizontal|bottom"

        android:padding="12dip"

        android:background="#AA000000"
        android:textColor="#ffffffff"

        android:text="Golden Gate" />

merge>

上面就是具体的代码,我们使用merge就表示我们的merge标签里面的ImageView和TextView的父布局就是merge的父布局FrameLayout,merge它不属于一个布局层次。下面我们再来看看整个布局层次。

【UI布局优化】Android布局优化的几种方式_第2张图片

从上图应该就一目了然了,总结一点:如果可以重用父布局,我们就可以使用merge,这样就减少了一个布局层次,这样可以加快UI的解析速度。

3、延迟加载
标签最大的优点是当你需要时才会加载,使用他并不会影响UI初始化时的性能,它 是一个不可见的,大小为0的View,最佳用途就是实现View的延迟加载,避免资源浪费,在需要的时候才加载View。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="内容1"/>

    <ViewStub
        android:id="@+id/pic_stub"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:inflatedId="@+id/pic_view_id_after_inflate"
        android:layout="@layout/pic_view" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="内容2"/>

    <Button
        android:text="加载ViewStub"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="startService"/>

LinearLayout>

最开始使用setContentView(R.layout.activity_main)的时候,ViewStub只是起到一个占位符的作用,它并不会占用空间,所以对其他的布局没有影响。

当我们点击Button的时候,我们就可以把ViewStub的layout属性指定的布局加载进来,用它来替换ViewStub,这样就把我们需要加载的内容加载进来了。具体的使用方式有两种:
1、通过findViewById找到ViewStub,然后直接调用setVisibility,这样它就会把layout里面指定的布局添加进来。

((ViewStub) findViewById(R.id.stub_import)).setVisibility(View.VISIBLE);  

2、通过findViewById找到ViewStub,然后直接调用inflate函数,使用这样方式的好处就是它可以将加载的布局View返回去,这样我们就可以拿到这个View进行相应的操作了。

View importPanel = ((ViewStub) findViewById(R.id.stub_import)).inflate();  

我们需要主要的是,在加载之前,我们通过pic_stub这个id来找到ViewStub,在加载之后,如果我们再希望获取到加载进来的这个布局的View,我们需要使用inflatedId这个属性指定的id来获取,因为在加载了布局之后,原来ViewStub的id会被inflatedId指定的这个id覆盖。

二、源码分析上面三种方式的过程

我们一般通过LayoutInflater来加载一个布局文件,这对这个不太明白的可以看看这篇文章Android获取到inflate服务的方式及inflate的解析过程.

我们知道它是通过Pull解析器来解析布局文件的,它在解析一个布局文件的时候,最终会执行rInflate函数,在Android获取到inflate服务的方式及inflate的解析过程这篇文章具体讲解它的过程,我们主要来分析分析这个函数。

void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs,
        boolean finishInflate) throws XmlPullParserException, IOException {

    final int depth = parser.getDepth();
    int type;

    while (((type = parser.next()) != XmlPullParser.END_TAG ||
            parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {

        if (type != XmlPullParser.START_TAG) {
            continue;
        }

        final String name = parser.getName();

        if (TAG_REQUEST_FOCUS.equals(name)) {
            parseRequestFocus(parser, parent);
        } else if (TAG_INCLUDE.equals(name)) {
            if (parser.getDepth() == 0) {
                throw new InflateException(" cannot be the root element");
            }
            parseInclude(parser, parent, attrs);
        } else if (TAG_MERGE.equals(name)) {
            throw new InflateException(" must be the root element");
        } else if (TAG_1995.equals(name)) {
            final View view = new BlinkLayout(mContext, attrs);
            final ViewGroup viewGroup = (ViewGroup) parent;
            final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
            rInflate(parser, view, attrs, true);
            viewGroup.addView(view, params);                
        } else {
            final View view = createViewFromTag(parent, name, attrs);
            final ViewGroup viewGroup = (ViewGroup) parent;
            final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
            rInflate(parser, view, attrs, true);
            viewGroup.addView(view, params);
        }
    }

    if (finishInflate) parent.onFinishInflate();
}

在解析标签的时候,它会根据不同的标签进行不同的处理,我们来看看它的过程。
1、如果这个标签为include标签

if (TAG_INCLUDE.equals(name)) {
    if (parser.getDepth() == 0) {
        throw new InflateException(" cannot be the root element");
    }
    parseInclude(parser, parent, attrs);
}

它会执行parseInclude函数,我们来看看它的处理。

private void parseInclude(XmlPullParser parser, View parent, AttributeSet attrs)
        throws XmlPullParserException, IOException {

    int type;
    // 1、判断父布局是否为一个ViewGroup实例
    if (parent instanceof ViewGroup) {
        // 2、得到include标签中layout属性的值,它就是重用布局
        final int layout = attrs.getAttributeResourceValue(null, "layout", 0);
        if (layout == 0) {
            final String value = attrs.getAttributeValue(null, "layout");
            if (value == null) {
                throw new InflateException("You must specifiy a layout in the"
                        + " include tag: ");
            } else {
                throw new InflateException("You must specifiy a valid layout "
                        + "reference. The layout ID " + value + " is not valid.");
            }
        } else {
            // 3、解析重用布局文件
            final XmlResourceParser childParser =
                    getContext().getResources().getLayout(layout);

            try {
                final AttributeSet childAttrs = Xml.asAttributeSet(childParser);

                while ((type = childParser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                    // Empty.
                }

                if (type != XmlPullParser.START_TAG) {
                    throw new InflateException(childParser.getPositionDescription() +
                            ": No start tag found!");
                }

                final String childName = childParser.getName();

                if (TAG_MERGE.equals(childName)) {
                    // Inflate all children.
                    rInflate(childParser, parent, childAttrs, false);
                } else {
                    final View view = createViewFromTag(parent, childName, childAttrs);
                    final ViewGroup group = (ViewGroup) parent;

                    // We try to load the layout params set in the  tag. If
                    // they don't exist, we will rely on the layout params set in the
                    // included XML file.
                    // During a layoutparams generation, a runtime exception is thrown
                    // if either layout_width or layout_height is missing. We catch
                    // this exception and set localParams accordingly: true means we
                    // successfully loaded layout params from the  tag,
                    // false means we need to rely on the included layout params.
                    ViewGroup.LayoutParams params = null;
                    try {
                        params = group.generateLayoutParams(attrs);
                    } catch (RuntimeException e) {
                        params = group.generateLayoutParams(childAttrs);
                    } finally {
                        if (params != null) {
                            view.setLayoutParams(params);
                        }
                    }

                    // Inflate all children.
                    rInflate(childParser, view, childAttrs, true);

                    // Attempt to override the included layout's android:id with the
                    // one set on the  tag itself.
                    TypedArray a = mContext.obtainStyledAttributes(attrs,
                        com.android.internal.R.styleable.View, 0, 0);
                    int id = a.getResourceId(com.android.internal.R.styleable.View_id, View.NO_ID);
                    // While we're at it, let's try to override android:visibility.
                    int visibility = a.getInt(com.android.internal.R.styleable.View_visibility, -1);
                    a.recycle();

                    if (id != View.NO_ID) {
                        view.setId(id);
                    }

                    switch (visibility) {
                        case 0:
                            view.setVisibility(View.VISIBLE);
                            break;
                        case 1:
                            view.setVisibility(View.INVISIBLE);
                            break;
                        case 2:
                            view.setVisibility(View.GONE);
                            break;
                    }
                    // 4、把解析处理的View加入到父布局中
                    group.addView(view);
                }
            } finally {
                childParser.close();
            }
        }
    } else {
        throw new InflateException(" can only be used inside of a ViewGroup");
    }

    final int currentDepth = parser.getDepth();
    while (((type = parser.next()) != XmlPullParser.END_TAG ||
            parser.getDepth() > currentDepth) && type != XmlPullParser.END_DOCUMENT) {
        // Empty
    }
}

上面展示它的整个过程,它就是将layout指定的这个布局文件进行解析,然后加入父布局中.

2、如果这个标签为merge标签

if (TAG_MERGE.equals(name)) {
    throw new InflateException(" must be the root element");
}

它里面抛出了一个异常,对应merge的使用,我们要具体的根据场合而定,具体要看父布局是否能够被重用,并且它要为根布局。在上面include的标签的解析中可以看到merge标签的处理过程。

if (TAG_MERGE.equals(childName)) {
    // Inflate all children.
    rInflate(childParser, parent, childAttrs, false);
}

从这里可以可以看到,如果是merge标签,就直接解析它的所有子元素,也就是说merge的父布局就是它内部子元素的父布局。

3、对于ViewStub,它会跟其他控件一样,实例化一个ViewStub对象

下面我来重点看看ViewStub类的setVisibility和inflate函数

首先我们需要看的是ViewStub的构造函数:

public ViewStub(Context context, AttributeSet attrs, int defStyle) {
    TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.ViewStub,
            defStyle, 0);

    //得到属性inflatedId的值
    mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
    //得到属性layout的值
    mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);

    a.recycle();

    a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.View, defStyle, 0);
    mID = a.getResourceId(R.styleable.View_id, NO_ID);
    a.recycle();

    initialize(context);
}

private void initialize(Context context) {
    mContext = context;
    // 从这里可以看到最开始这个控件的可见性为GONE
    setVisibility(GONE);
    // 这里先对它不进行绘制
    setWillNotDraw(true);
}

上面的工作就是两点:
1、获取各个属性的值
2、设置ViewStub的可见性为GONE,也就是它不占位置,并且也不绘制,因为它不是真正要显示的View

下面看看ViewStub的inflate函数:

public View inflate() {
    final ViewParent viewParent = getParent();

    if (viewParent != null && viewParent instanceof ViewGroup) {
        if (mLayoutResource != 0) {
            final ViewGroup parent = (ViewGroup) viewParent;
            final LayoutInflater factory;
            if (mInflater != null) {
                factory = mInflater;
            } else {
                factory = LayoutInflater.from(mContext);
            }
            // 这里直接解析mLayoutResource这个布局,也就是上面得到的layout属性值
            final View view = factory.inflate(mLayoutResource, parent,
                    false);
            // 这里会对这个布局设置id,也就是inflatedId的属性值
            if (mInflatedId != NO_ID) {
                view.setId(mInflatedId);
            }

            // 这里从父布局中找到这个viewstub的index
            final int index = parent.indexOfChild(this);
            //这里将viewstub这个占位view移除
            parent.removeViewInLayout(this);

            // 这里会把这个view添加到父布局指定的index中去,也就实现了对viewstub的替换
            final ViewGroup.LayoutParams layoutParams = getLayoutParams();
            if (layoutParams != null) {
                parent.addView(view, index, layoutParams);
            } else {
                parent.addView(view, index);
            }

            //这里会把这个view弱引用到mInflatedViewRef
            mInflatedViewRef = new WeakReference(view);

            // 如果设置了回调,就会调用回调函数
            if (mInflateListener != null) {
                mInflateListener.onInflate(this, view);
            }

            return view;
        } else {
            throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
        }
    } else {
        throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
    }
}

上面的工作可以总结为以下几点:
1、解析出layout属性赋值的布局,得到对应的View
2、为这个View指定id
3、找到ViewStub在父布局的索引,然后将ViewStub移除
4、将上面解析的View加入到父布局的指定索引处

上面的整个过程总结一点就是:使用给定的布局来替换ViewStub,达到动态加载的目的,ViewStub仅仅只是一个占位View.

下面看看setVisibility函数的源码。

public void setVisibility(int visibility) {
    if (mInflatedViewRef != null) {
        View view = mInflatedViewRef.get();
        if (view != null) {
            view.setVisibility(visibility);
        } else {
            throw new IllegalStateException("setVisibility called on un-referenced view");
        }
    } else {
        super.setVisibility(visibility);
        if (visibility == VISIBLE || visibility == INVISIBLE) {
            inflate();
        }
    }
}

它的处理可以看到,首先看mInflatedViewRef是否为空,上面在inflate中,我们看到它会把解析处理的view弱引用到mInflatedViewRef,如果不为空,就可以直接得到这个View,然后设置它为可见。如果为空,这样就会执行inflate方法。就是上面的那个方法。

参考文章:

Android Layout Trick #2: Include to Reuse

Android Layout Tricks #3: Optimize by merging

Android Layout Tricks #3: Optimize with stubs

Android抽象布局——include、merge 、ViewStub

你可能感兴趣的:(Android性能优化)