美文网首页
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