Android布局優(yōu)化/ViewStub/merge/include源碼閱讀

經(jīng)常可能會(huì)被問(wèn)到形如以下的問(wèn)題:
1.為什么在Android中使用ViewStub/merge/include可以幫我們完成布局優(yōu)化?
2.為什么ViewStub可以做到不占用布局資源/懶加載?
3.merge標(biāo)簽為什么能做到減少嵌套?
4.阿森納
5.為什么ViewStub多次調(diào)用inflate的報(bào)錯(cuò)?
....

目錄

1.ViewStub初始化解析
2.ViewStub使用
3.ViewStub相關(guān)問(wèn)題

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

4.include標(biāo)簽使用
5.include標(biāo)簽解析
6.merge標(biāo)簽使用
7.merge標(biāo)簽解析
8.merge標(biāo)簽問(wèn)題
9.參考資料

ViewStub初始化解析

ViewStub是View類(lèi)的子類(lèi),其構(gòu)造函數(shù)如下

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

構(gòu)造函數(shù)先調(diào)用了一次setVisibility()和setWillNotDraw()方法;

ViewStub復(fù)寫(xiě)了父類(lèi)的setVisibility方法,在沒(méi)有inflate之前,ViewStub的mInflatedViewRef是null,visibility為gone,所以這里是調(diào)用父類(lèi)里的setVisibility(visibility)方法,完成flag的設(shè)置,可以簡(jiǎn)單的理解為:不可見(jiàn)

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

然后直接調(diào)用的父類(lèi)setWillNotDraw方法,也就是告訴view:"viewStub暫時(shí)不繪制"

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

onMeasure方法直接設(shè)置寬高為0

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

以上是ViewStub的初始化過(guò)程做的事,回答了開(kāi)頭的第二個(gè)問(wèn)題,
『ViewStub是一個(gè)不可見(jiàn)的,不繪制,大小為0的視圖。』

ViewStub使用

當(dāng)業(yè)務(wù)需要顯示ViewStub里的布局時(shí),調(diào)用setVisibility方法,可見(jiàn)性設(shè)為true

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

實(shí)際上是調(diào)用了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.調(diào)用getParent方法拿父View,父View為空(為空怎么添加嘛)或者父view不是viewGroup(不是VG怎么添加?)拋出異常;

2.父view檢測(cè)正常后,調(diào)用inflateViewNoAdd方法,其本質(zhì)上是從layout文件里生成一個(gè)view

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

這也是為什么ViewStub是懶加載的原因,只有當(dāng)ViewStub被要求setVisible(Visible)的時(shí)候才初始化該view。

看到這個(gè)inflate方法的第三個(gè)參數(shù)attachToRoot為false,這個(gè)也解釋了為什么ViewStub里使用的layout根標(biāo)簽不能為merge標(biāo)簽,報(bào)錯(cuò)堆棧更加明了:

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.調(diào)用replaceSelfWithView,從父view中找到自己ViewStub,刪除自己這個(gè)節(jié)點(diǎn),然后把生產(chǎn)的view加到這個(gè)位置,完成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);
        }
    }

這個(gè)就回答了為什么多次inflate會(huì)報(bào)空指針錯(cuò)誤,這里已經(jīng)把自己刪除了,findViewById的時(shí)候就找不到了。

4.初始化一個(gè)弱引用,把view傳進(jìn)去;

mInflatedViewRef的作用呢,在后續(xù)再次調(diào)用setVisibility的時(shí)候,從mInflatedViewRef取出view,就不用再初始化view了。

5.回調(diào)OnInflateListener的inflate方法;

該回調(diào)的作用見(jiàn)注釋

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

linstener是ViewStubProxy代理類(lèi)里進(jìn)行設(shè)置
這個(gè)代理的作用???????(埋坑,先下班)
這個(gè)代理在哪里初始化???

ViewStub相關(guān)問(wèn)題

1.為什么ViewStub能優(yōu)化布局性能?
因?yàn)閂iewStub是一個(gè)不可見(jiàn),不繪制,0大小的View,可以做到懶加載。

2.ViewStub懶加載的原理是?
它的inflate過(guò)程是在初次要求可見(jiàn)的時(shí)候進(jìn)行的,也就是按需加載。

3.ViewStub的layout能用merge做根標(biāo)簽么?
不能,因?yàn)閙erge的布局要求attachToRoot為true,而ViewStub內(nèi)部實(shí)現(xiàn)inflate布局的方法,attachToRoot為false。

4.ViewStub標(biāo)簽內(nèi)能加入其他view么?
不能,ViewStub是一個(gè)自閉合標(biāo)簽,引用的布局通過(guò)layout屬性進(jìn)行引用,需另外寫(xiě)xml布局文件。

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

5.ViewStub多次調(diào)用inflate/setVisible會(huì)發(fā)生什么情況?

  • 如果ViewStub是局部變量,多次調(diào)用其首先會(huì)通過(guò)findViewById的方法去找ViewStub,后續(xù)會(huì)返回null,調(diào)用inflate/setVisibility時(shí)會(huì)報(bào)NPE
 java.lang.NullPointerException: Attempt to invoke virtual method 
'android.view.View android.view.ViewStub.inflate()' on a null object reference

原因是初次inflate后會(huì)內(nèi)部調(diào)用replaceSelfWithView方法,把viewStub節(jié)點(diǎn)從ViewTree里刪除。

  • 如果ViewStub是全局變量,多次調(diào)用inflate,會(huì)拋出異常
  java.lang.IllegalStateException: ViewStub must have a non-null ViewGroup viewParent

因?yàn)槌醮蝘nflate之后自己已經(jīng)從ViewTree中刪除了,但是inflate會(huì)先判斷能不能拿到viewStub自己的parentView,后續(xù)是拿不到即拋出異常。

多次調(diào)用setVisible沒(méi)有問(wèn)題,因?yàn)橹粫?huì)調(diào)用一次inflate,內(nèi)部是通過(guò)mInflatedViewRef拿view。

如果還想再次顯示ViewStub 引用的布局/view(以下這種寫(xiě)法),則建議主動(dòng)try catch這些異常。

try {
      View viewStub = viewStub.inflate();     
    //inflate 方法只能被調(diào)用一次,
      hintText = (TextView) viewStub.findViewById(R.id.tv_vsContent);
      hintText.setText("沒(méi)有相關(guān)數(shù)據(jù),請(qǐng)刷新");
      } catch (Exception e) {
       viewStub.setVisibility(View.VISIBLE);
      } finally {
        hintText.setText("沒(méi)有相關(guān)數(shù)據(jù),請(qǐng)刷新");
    }
viewStub inflate前
viewStub inflate后

include標(biāo)簽的使用

就很簡(jiǎn)單的在xml里引入

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

為什么那么簡(jiǎn)單,因?yàn)閷?shí)際上就是對(duì)xml的解析,遇到了include后進(jìn)行處理,源碼在LayoutInflater的rInflate方法里。

include標(biāo)簽解析

void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
....這里只關(guān)注include標(biāo)簽的解析...
 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標(biāo)簽中沒(méi)有設(shè)置layout屬性,會(huì)拋出異常  
            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());

            }

            // 這里可能會(huì)出現(xiàn)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標(biāo)簽里load layoutParams時(shí),
                      //在父view拿不到的情況下希望能catch住異常,
                      //true是正常情況,false是異常情況,
                      //但是會(huì)生成一個(gè)localParams給設(shè)置上去。
                        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使用

使用該標(biāo)簽的幾種情況:

  • 如果要 include 的子布局的根標(biāo)簽是 Framelayout,那么最好替換為 merge,這樣可以減少嵌套;

  • 如果父布局是LinearLayout,include的子布局也是LinearLayout且兩者方向一致,也可以用merge減少嵌套,因會(huì)忽略merge里的方向?qū)傩裕?/p>

  • 如果子布局直接以一個(gè)控件為根節(jié)點(diǎn),也就是只有一個(gè)控件的情況,這時(shí)就沒(méi)必要再使用 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標(biāo)簽解析

同樣在LayoutInflator的rInflate方法里

void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
....這里只關(guān)注include標(biāo)簽的解析...
       } else if (TAG_MERGE.equals(name)) {
                throw new InflateException("<merge /> must be the root element");
   ...源碼有省略...
}

直接拋出異常,因?yàn)閙erge標(biāo)簽必須做根標(biāo)簽。
說(shuō)白了其實(shí)就是遇到merge標(biāo)簽,那么直接將其中的子元素添加到merge標(biāo)簽父view中,這樣就保證了不會(huì)引入額外的層級(jí),也同時(shí)忽略了merge里的attr屬性。

merge標(biāo)簽問(wèn)題

1.LayoutInflator的inflate方法對(duì)與merge標(biāo)簽的處理說(shuō)明,要被附加的父級(jí)view如果為空,但是要求attachToRoot,那么拋出異常,因?yàn)楦靖郊硬簧先グ ?/p>

 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 /> 只能作為布局的根標(biāo)簽使用;
3.不要使用 <merge /> 作為 ListView Item 的根節(jié)點(diǎn);
4.<merge /> 標(biāo)簽不需要設(shè)置屬性,寫(xiě)了也不起作用,因?yàn)橄到y(tǒng)會(huì)忽略 <merge /> 標(biāo)簽;
5.inflate 以 <merge /> 為根標(biāo)簽的布局時(shí)要注意
~5.1必須指定一個(gè)父 ViewGroup;
~5.2必須設(shè)定 attachToRoot 為 true;
也就是說(shuō) inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) 方法的二個(gè)參數(shù) root 不能為 null,并且第三參數(shù) attachToRoot 必須傳 true

參考資料

Android布局優(yōu)化之ViewStub、include、merge使用與源碼分析
http://www.androidchina.net/2485.html

ViewStub--使用介紹
http://www.lxweimin.com/p/175096cd89ac

使用<merge/>標(biāo)簽減少布局層級(jí)
http://www.lxweimin.com/p/278350aa0048

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容