在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