美文网首页
android优化:布局文件优化

android优化:布局文件优化

作者: 红发_SHANKS | 来源:发表于2018-03-06 10:29 被阅读46次

布局优化是我们 app 优化的第一步,通过Android studio 提供的 Layout Inspector 可以很直接的看到冗余层级,去除这些冗余层级将使我们的 UI 变的更流畅。另外官方不推荐继续使用**** hierarchy Viewer****查看视图层级了。

官方文档

include 布局

随着开发的页面越来越多,难免会有部分页面的布局中的部分布局重复,比如每个页面的titleView,这些重复的布局不仅仅是冗余代码,后续需要更改调整的时候,需要每个页面都进行调整,极容易出错。

Android 提供的 include 标签可以将一个布局加入到另外一个布局中,这样,重复的布局就可以单独写入一个布局 XML 文件中,方便管理、修改,提高了复用性。

使用示例:

<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"
    tools:showIn="@layout/activity_main" // showIn 标签在编译过程中会被删除,仅供开发过程中预览使用
    android:gravity="center_horizontal">

    <include layout="@layout/titlebar"/>

    // include 标签可以包含完整的 layout_* 属性
    <include android:id="@+id/news_title"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         layout="@layout/title"/>

    <TextView android:layout_width="match_parent"
              android:layout_height="wrap_content"
              tools:text="@string/hello" // 文字仅预览时可见,编译过程中会删除
              android:padding="10dp" />

    ...

</LinearLayout>

include 标签的原理很简单,就是在 解析布局的时候,如果检测到 include 标签,就把该布局下的跟视图添加到 include 所在的父视图中。对于 xml 的解析,最终都会调用 LayoutInflater 的 inflate 方法,该方法最终又会调用到 rInflate 方法,我们看看这个方法:

    // 省略其他代码
        } else if (TAG_INCLUDE.equals(name)) {
                if (parser.getDepth() == 0) {
                    throw new InflateException("<include /> cannot be the root element");
                }
                parseInclude(parser, context, parent, attrs);
                }

从这个 if 条件判断汇总,我们能看到, include 标签不能作为跟标签,否则会抛出异常,继续看看 parseInclude 方法:

    private void parseInclude(XmlPullParser parser, Context context, View parent,
            AttributeSet attrs) throws XmlPullParserException, IOException {
        int type;

        if (parent instanceof ViewGroup) {
            // If the layout is pointing to a theme attribute, we have to
            // massage the value to get a resource identifier out of it.
            int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0);
            if (layout == 0) { // include 标签必须包含 layout 属性。
                final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
                if (value == null || value.length() <= 0) {
                    throw new InflateException("You must specify a layout in the"
                            + " include tag: <include layout=\"@layout/layoutID\" />");
                }

                // Attempt to resolve the "?attr/name" string to an attribute
                // within the default (e.g. application) package.
                layout = context.getResources().getIdentifier(
                        value.substring(1), "attr", context.getPackageName());

            }

            // The layout might be referencing a theme attribute.
            if (mTempValue == null) {
                mTempValue = new TypedValue();
            }
            if (layout != 0 && context.getTheme().resolveAttribute(layout, mTempValue, true)) {
                layout = mTempValue.resourceId;
            }

            if (layout == 0) {
                final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
                throw new InflateException("You must specify a valid layout "
                        + "reference. The layout ID " + value + " is not valid.");
            } else {
                final XmlResourceParser childParser = context.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();
                    // merge 标签
                    if (TAG_MERGE.equals(childName)) {
                        // The <merge> tag doesn't support android:theme, so
                        // nothing special to do here.
                        rInflate(childParser, parent, context, childAttrs, false);
                    } else {
                        final View view = createViewFromTag(parent, childName,
                                context, childAttrs, hasThemeOverride);
                        final ViewGroup group = (ViewGroup) parent;

                        final TypedArray a = context.obtainStyledAttributes(
                                attrs, R.styleable.Include);
                        final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);
                        final int visibility = a.getInt(R.styleable.Include_visibility, -1);
                        a.recycle();

                        // We try to load the layout params set in the <include /> tag.
                        // If the parent can't generate layout params (ex. missing width
                        // or height for the framework ViewGroups, though this is not
                        // necessarily true of all ViewGroups) then we expect it to throw
                        // a runtime exception.
                        // 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) {
                            // Ignore, just fail over to child attrs.
                        }
                        if (params == null) {
                            params = group.generateLayoutParams(childAttrs);
                        }
                        view.setLayoutParams(params);

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

                        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;
                        }

                        group.addView(view);
                    }
                } finally {
                    childParser.close();
                }
            }
        } else {
            throw new InflateException("<include /> can only be used inside of a ViewGroup");
        }

        LayoutInflater.consumeChildElements(parser);
    }

merge 标签

如果父视图布局和子视图的根布局一样,那么可以使用 merge 标签来减少视图嵌套层级。在解析 xml 的时候,会自动忽略 merge 标签,直接加载 merge 中的视图。这样,我们就少了一个层级的嵌套,减少了层级,提升了布局性能。

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

    <Button
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="@string/add"/>

    <Button
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="@string/delete"/>

</merge>

merge 标签原理也比较简单,看看 LayoutInflater 的 inflate 方法:

else if (TAG_MERGE.equals(name)) {
                throw new InflateException("<merge /> must be the root element");
            } 

在 while 循环中遍历子视图的时候,如果遍历到了 merge 标签会报错,因为 merge 标签只能作为跟标签。

 else {
     // 根据 tag 创建视图
     final View view = createViewFromTag(parent, name, context, attrs);
     // 获取 merge 父视图
     final ViewGroup viewGroup = (ViewGroup) parent;
     // 获取params
     final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
     // 加载子视图
     rInflateChildren(parser, view, attrs, true);
     // 添加到父视图
     viewGroup.addView(view, params);
            }

ViewStub 视图

ViewStub是一个不可见的和能在运行时期间延迟加载视图的,宽高都为 0 的视图。当调用 ViewStub 的 inflate 方法 或者是 setVisiable 以后才加载视图,变的可见。这对于某些需要根据特性情况来进行加载的视图比较有效,可以在正常初始化视图的时候节省部分系统资源。

    <ViewStub
        android:id="@+id/view_stub"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:layout="@layout/view_stub_main" /> // 注意这里有 android 标签

看看 ViewStub 源码:

public ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context);

        final TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.ViewStub, defStyleAttr, defStyleRes);
        mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
        mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);
        mID = a.getResourceId(R.styleable.ViewStub_id, NO_ID);
        a.recycle();

        setVisibility(GONE);
        // 默认是不绘制的
        setWillNotDraw(true);
    }
    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();
            }
        }
    }
 public View inflate() {
        final ViewParent viewParent = getParent();

        if (viewParent != null && viewParent instanceof ViewGroup) {
            if (mLayoutResource != 0) {
                final ViewGroup parent = (ViewGroup) viewParent;
                // 加载布局
                final View view = inflateViewNoAdd(parent);
                // 使用自身布局替换父布局中的 ViewStub 标签
                replaceSelfWithView(view, parent);

                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");
        }
    }
    private View inflateViewNoAdd(ViewGroup parent) {
        final LayoutInflater factory;
        if (mInflater != null) {
            factory = mInflater;
        } else {
            factory = LayoutInflater.from(mContext);
        }
        // 加载布局
        final View view = factory.inflate(mLayoutResource, parent, false);

        if (mInflatedId != NO_ID) {
            view.setId(mInflatedId);
        }
        return view;
    }
    private void replaceSelfWithView(View view, ViewGroup parent) {
        final int index = parent.indexOfChild(this);
        // 从父布局中删除 ViewStub 
        parent.removeViewInLayout(this);
        // 将自身布局添加到父布局
        final ViewGroup.LayoutParams layoutParams = getLayoutParams();
        if (layoutParams != null) {
            parent.addView(view, index, layoutParams);
        } else {
            parent.addView(view, index);
        }
    }

总结

  • 尽量使用 RelativeLayout 避免视图嵌套
  • 使用<merge>标签减少视图嵌套层数
  • 使用 ViewStub 加载需要延迟加载的视图
  • 在 RecyclerView 等列表视图中,尽量少使用 Lyout_weight 属性
  • 将可以复用的视图抽取出来使用 <include> 标签加载

相关文章

网友评论

      本文标题:android优化:布局文件优化

      本文链接:https://www.haomeiwen.com/subject/urjlfftx.html