Android布局优化/ViewStub/merge/include源码阅读

经常可能会被问到形如以下的问题:
1.为什么在Android中使用ViewStub/merge/include可以帮我们完成布局优化?
2.为什么ViewStub可以做到不占用布局资源/懒加载?
3.merge标签为什么能做到减少嵌套?
4.阿森纳
5.为什么ViewStub多次调用inflate的报错?
....

目录

1.ViewStub初始化解析
2.ViewStub使用
3.ViewStub相关问题

inflate源码解析
https://mp.weixin.qq.com/s/xHUeKc0xL2Si4-PJOIBVWQ

4.include标签使用
5.include标签解析
6.merge标签使用
7.merge标签解析
8.merge标签问题
9.参考资料

ViewStub初始化解析

ViewStub是View类的子类,其构造函数如下

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

构造函数先调用了一次setVisibility()和setWillNotDraw()方法;

ViewStub复写了父类的setVisibility方法,在没有inflate之前,ViewStub的mInflatedViewRef是null,visibility为gone,所以这里是调用父类里的setVisibility(visibility)方法,完成flag的设置,可以简单的理解为:不可见

    @Override
    @android.view.RemotableViewMethod(asyncImpl = "setVisibilityAsync")
        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();
                }
            }
        }

然后直接调用的父类setWillNotDraw方法,也就是告诉view:"viewStub暂时不绘制"

    public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }

onMeasure方法直接设置宽高为0

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(0, 0);
    }

以上是ViewStub的初始化过程做的事,回答了开头的第二个问题,
『ViewStub是一个不可见的,不绘制,大小为0的视图。』

ViewStub使用

当业务需要显示ViewStub里的布局时,调用setVisibility方法,可见性设为true

    ViewStub viewStub = findViewById(R.id.viewStub);
    viewStub.setVisibility(View.VISIBLE);

实际上是调用了ViewStub的私有inflate方法

    public View inflate() {
            final ViewParent viewParent = getParent();
    
            if (viewParent != null && viewParent instanceof ViewGroup) {//#1
                if (mLayoutResource != 0) {
                    final ViewGroup parent = (ViewGroup) viewParent;
                    final View view = inflateViewNoAdd(parent);//#2
                    replaceSelfWithView(view, parent);//#3
    
                    mInflatedViewRef = new WeakReference<>(view);//#4
                    if (mInflateListener != null) {
                        mInflateListener.onInflate(this, view);//#5
                    }
    
                    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.调用getParent方法拿父View,父View为空(为空怎么添加嘛)或者父view不是viewGroup(不是VG怎么添加?)抛出异常;

2.父view检测正常后,调用inflateViewNoAdd方法,其本质上是从layout文件里生成一个view

    final View view = factory.inflate(mLayoutResource, parent, false);

这也是为什么ViewStub是懒加载的原因,只有当ViewStub被要求setVisible(Visible)的时候才初始化该view。

看到这个inflate方法的第三个参数attachToRoot为false,这个也解释了为什么ViewStub里使用的layout根标签不能为merge标签,报错堆栈更加明了:

android.view.InflateException: Binary XML file line #2: <merge /> can be used only with a valid ViewGroup root and attachToRoot=true
    Caused by: android.view.InflateException: <merge /> can be used only with a valid ViewGroup root and attachToRoot=true
        at android.view.LayoutInflater.inflate(LayoutInflater.java:485)
        at android.view.LayoutInflater.inflate(LayoutInflater.java:423)
        at android.view.ViewStub.inflateViewNoAdd(ViewStub.java:269)
        at android.view.ViewStub.inflate(ViewStub.java:302)
        at android.view.ViewStub.setVisibility(ViewStub.java:247)

3.调用replaceSelfWithView,从父view中找到自己ViewStub,删除自己这个节点,然后把生产的view加到这个位置,完成replace;

    private void replaceSelfWithView(View view, ViewGroup parent) {
        final int index = parent.indexOfChild(this);
        parent.removeViewInLayout(this);
    
        final ViewGroup.LayoutParams layoutParams = getLayoutParams();
        if (layoutParams != null) {
            parent.addView(view, index, layoutParams);
        } else {
            parent.addView(view, index);
        }
    }

这个就回答了为什么多次inflate会报空指针错误,这里已经把自己删除了,findViewById的时候就找不到了。

4.初始化一个弱引用,把view传进去;

mInflatedViewRef的作用呢,在后续再次调用setVisibility的时候,从mInflatedViewRef取出view,就不用再初始化view了。

5.回调OnInflateListener的inflate方法;

该回调的作用见注释

    Listener used to receive a notification after a ViewStub has 
successfully inflated its layout resource.

linstener是ViewStubProxy代理类里进行设置
这个代理的作用???????(埋坑,先下班)
这个代理在哪里初始化???

ViewStub相关问题

1.为什么ViewStub能优化布局性能?
因为ViewStub是一个不可见,不绘制,0大小的View,可以做到懒加载。

2.ViewStub懒加载的原理是?
它的inflate过程是在初次要求可见的时候进行的,也就是按需加载。

3.ViewStub的layout能用merge做根标签么?
不能,因为merge的布局要求attachToRoot为true,而ViewStub内部实现inflate布局的方法,attachToRoot为false。

4.ViewStub标签内能加入其他view么?
不能,ViewStub是一个自闭合标签,引用的布局通过layout属性进行引用,需另外写xml布局文件。

<ViewStub
        android:id="@+id/viewStub"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout="@layout/vs_content" />

5.ViewStub多次调用inflate/setVisible会发生什么情况?

  • 如果ViewStub是局部变量,多次调用其首先会通过findViewById的方法去找ViewStub,后续会返回null,调用inflate/setVisibility时会报NPE
 java.lang.NullPointerException: Attempt to invoke virtual method 
'android.view.View android.view.ViewStub.inflate()' on a null object reference

原因是初次inflate后会内部调用replaceSelfWithView方法,把viewStub节点从ViewTree里删除。

  • 如果ViewStub是全局变量,多次调用inflate,会抛出异常
  java.lang.IllegalStateException: ViewStub must have a non-null ViewGroup viewParent

因为初次inflate之后自己已经从ViewTree中删除了,但是inflate会先判断能不能拿到viewStub自己的parentView,后续是拿不到即抛出异常。

多次调用setVisible没有问题,因为只会调用一次inflate,内部是通过mInflatedViewRef拿view。

如果还想再次显示ViewStub 引用的布局/view(以下这种写法),则建议主动try catch这些异常。

try {
      View viewStub = viewStub.inflate();     
    //inflate 方法只能被调用一次,
      hintText = (TextView) viewStub.findViewById(R.id.tv_vsContent);
      hintText.setText("没有相关数据,请刷新");
      } catch (Exception e) {
       viewStub.setVisibility(View.VISIBLE);
      } finally {
        hintText.setText("没有相关数据,请刷新");
    }
viewStub inflate前
viewStub inflate后

include标签的使用

就很简单的在xml里引入

<include  
        android:id="@+id/my_title_ly"  
        android:layout_width="match_parent"  
        android:layout_height="wrap_content"  
        layout="@layout/my_title_layout" />  

为什么那么简单,因为实际上就是对xml的解析,遇到了include后进行处理,源码在LayoutInflater的rInflate方法里。

include标签解析

void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
....这里只关注include标签的解析...
 else if (TAG_INCLUDE.equals(name)) {
         if (parser.getDepth() == 0) {
              throw new InflateException("<include /> cannot be the root element");
        }
         parseInclude(parser, context, parent, attrs);
    }
   ...源码有省略...
}

private void parseInclude(XmlPullParser parser, Context context, View parent,
            AttributeSet attrs) throws XmlPullParserException, IOException {
        int type;
        //父view必须是ViewGroup,否则抛出异常
        if (parent instanceof ViewGroup) {
            //处理主题
            final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
            final int themeResId = ta.getResourceId(0, 0);
            final boolean hasThemeOverride = themeResId != 0;
            if (hasThemeOverride) {
                context = new ContextThemeWrapper(context, themeResId);
            }
            ta.recycle();

            //include标签中没有设置layout属性,会抛出异常  
            int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0);
            if (layout == 0) {
                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\" />");
                }
                layout = context.getResources().getIdentifier(
                        value.substring(1), "attr", context.getPackageName());

            }

            // 这里可能会出现layout是从theme里引用的情况
            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();

                    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.
                      //大意是从include标签里load layoutParams时,
                      //在父view拿不到的情况下希望能catch住异常,
                      //true是正常情况,false是异常情况,
                      //但是会生成一个localParams给设置上去。
                        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使用

使用该标签的几种情况:

  • 如果要 include 的子布局的根标签是 Framelayout,那么最好替换为 merge,这样可以减少嵌套;

  • 如果父布局是LinearLayout,include的子布局也是LinearLayout且两者方向一致,也可以用merge减少嵌套,因会忽略merge里的方向属性;

  • 如果子布局直接以一个控件为根节点,也就是只有一个控件的情况,这时就没必要再使用 merge 包裹了。

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="heng zhe 1" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="heng zhe 2" />
</merge>

merge标签解析

同样在LayoutInflator的rInflate方法里

void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
....这里只关注include标签的解析...
       } else if (TAG_MERGE.equals(name)) {
                throw new InflateException("<merge /> must be the root element");
   ...源码有省略...
}

直接抛出异常,因为merge标签必须做根标签。
说白了其实就是遇到merge标签,那么直接将其中的子元素添加到merge标签父view中,这样就保证了不会引入额外的层级,也同时忽略了merge里的attr属性。

merge标签问题

1.LayoutInflator的inflate方法对与merge标签的处理说明,要被附加的父级view如果为空,但是要求attachToRoot,那么抛出异常,因为根本附加不上去啊。

 if (TAG_MERGE.equals(name)) {
     if (root == null || !attachToRoot) {
      throw new InflateException("<merge /> can be used only with a valid "     
+ "ViewGroup root and attachToRoot=true");
      }
 rInflate(parser, root, inflaterContext, attrs, false);

2.<merge /> 只能作为布局的根标签使用;
3.不要使用 <merge /> 作为 ListView Item 的根节点;
4.<merge /> 标签不需要设置属性,写了也不起作用,因为系统会忽略 <merge /> 标签;
5.inflate 以 <merge /> 为根标签的布局时要注意
~5.1必须指定一个父 ViewGroup;
~5.2必须设定 attachToRoot 为 true;
也就是说 inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) 方法的二个参数 root 不能为 null,并且第三参数 attachToRoot 必须传 true

参考资料

Android布局优化之ViewStub、include、merge使用与源码分析
http://www.androidchina.net/2485.html

ViewStub--使用介绍
https://www.jianshu.com/p/175096cd89ac

使用<merge/>标签减少布局层级
https://www.jianshu.com/p/278350aa0048

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容