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

来源:互联网 发布:吉尼佛摄影吧淘宝 编辑:程序博客网 时间:2024/05/10 21:37

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

一、几种方式的用法
1、布局重用<include />

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

<LinearLayout xmlns:android="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">      <include  android:id="@+id/titlebar"              layout="@layout/titlebar"/>      <TextView android:layout_width=”match_parent”                android:layout_height="wrap_content"                android:text="@string/hello"                android:padding="10dp" />      ...  </LinearLayout>  

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

<?xml version="1.0" encoding="utf-8"?><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、减少视图层级<merge />

这个标签的作用就是删减多余的层级,优化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来查看它的布局层次。

这里写图片描述

从这个布局层次,就可以看到我们的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它不属于一个布局层次。下面我们再来看看整个布局层次。

这里写图片描述

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

3、延迟加载<ViewStub />
<ViewStub />标签最大的优点是当你需要时才会加载,使用他并不会影响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("<include /> cannot be the root element");            }            parseInclude(parser, parent, attrs);        } else if (TAG_MERGE.equals(name)) {            throw new InflateException("<merge /> 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("<include /> 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: <include layout=\"@layout/layoutID\" />");            } 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 <include /> 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 <include /> 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 <include /> 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("<include /> 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("<merge /> 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>(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

0 0
原创粉丝点击