ViewStub源碼分析

辛辛苦苦寫了一篇博客,發現簡書markdown插入代碼居然沒有行序,我都懵逼了。看來這個編輯器對程序猿不太友好啊,哈哈。
如果想要更好的閱讀體驗可以去我的CSDN博客https://blog.csdn.net/u012814441/article/details/80524501

1、ViewStub的使用

我們先來回憶一下平時的怎么使用ViewStub的。首先定義一個供ViewStub引用的布局文件,取名layout_view_stub.xml。代碼如下所示。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:layout_width="wrap_content"
        android:text="hello"
        android:layout_height="wrap_content" />
    <TextView
        android:layout_width="wrap_content"
        android:text="world"
        android:layout_height="wrap_content" />
</RelativeLayout>

接著定義一個布局文件,取名為activity_main.xml。代碼如下所示

<?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="match_parent"
    android:orientation="vertical">
    <Button
        android:id="@+id/one"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="clickThree"
        android:text="顯示" />
    <Button
        android:id="@+id/two"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="clickThree"
        android:text="隱藏" />
    <Button
        android:id="@+id/three"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="clickThree"
        android:text="加載" />
    <ViewStub
        android:id="@+id/vs"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout="@layout/layout_view_stub"/>
</LinearLayout>

在28行引用layout_view_stub布局資源。再接著寫寫邏輯代碼

final ViewStub viewStub = findViewById(R.id.vs);
Button button = findViewById(R.id.two);
button.setOnClickListener(new View.OnClickListener() {
    @Override
  public void onClick(View v) {
        viewStub.setVisibility(View.GONE);
  }
});

Button button1 = findViewById(R.id.one);
button1.setOnClickListener(new View.OnClickListener() {
    @Override
  public void onClick(View v) {
        viewStub.setVisibility(View.VISIBLE);
  }
});

Button button2 = findViewById(R.id.three);
button2.setOnClickListener(new View.OnClickListener() {
    @Override
  public void onClick(View v) {
        viewStub.inflate();
  }
});

效果如圖


<font color="#7f7f7f" style="box-sizing: border-box; outline: none !important;">git動畫</font>

2、ViewStub的疑問

  • 通過上面的演示我們已經知道一些基本用法,接下來針對平時使用提出幾個問題。
    1.為什么ViewStub的能夠優化布局?
    2.為什么ViewStub引用的布局的根節點不能為merge?
    3.為什么ViewStub的inflate方法連續調用兩次就拋出異常?
    4.inflate方法和setVisibility方法有什么區別?

3、ViewStub源碼分析

  • 首先我們需要知道什么是ViewStub?根據官方對ViewStub的定義。

它是一種不可見、零大小,可以在APP運行時動態加載布局資源的視圖。

  • ViewStub是一個繼承自View的控件,我們來看看它的繼承結構


    這里寫圖片描述

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

先是通過3~8行完成相關屬性初始化。第9行setVisibility(GONE)將ViewStub設置為不可見狀態。第10行設置為不繪制視圖。完成了初始化之后按照View的繪制流程,接著就會回調onMeasure方法。

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

    @Override
    public void draw(Canvas canvas) {
    }

在onMeasure方法中,將自身的寬度和高度設置為0。并且重寫了draw方法,不對此視圖進行渲染繪制。通過這兩個方法我們可以解決第一個問題。"為什么ViewStub的能夠優化布局?"
  當我們在布局中使用ViewStub時,此時它是0大小,不進行繪制的控件。其初始化代價小到可以忽略(真正的布局只有在調用infalte或setVisibility才會被加載),這樣能夠加快整個布局的初始化。因此可以說是優化了布局性能。
  在完成了整個ViewStub的初始化之后,我們需要主動調用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);
                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");
        }
    }

首先看2、3行,拿到了該視圖的父節點(LinearLayout)之后,判斷其是否為空且是否為ViewGroup。如果為空且不是ViewGroup就會拋出IllegalStateException異常。
  第4~14行,先判斷mLayoutResource是否有引用布局資源,如果沒有去到14行拋出IllegalArgumentException異常。如果有的話就調用inflateViewNoAdd方法,獲取加載布局資源view。接著調用replaceSelfWithView方法,將ViewStub自身替換為view。再初始化一個弱引用來存放view對象(方便用戶調用setVisibility方法復用對象)。最后9~10行如果你事先注冊了setOnInflateListener方法,此時會回調onInflate方法告訴你加載布局資源成功。
  至此整個inflate方法執行完畢。但是我們還有3個疑問沒有解決。先解決第2個問題“為什么ViewStub引用的layout布局的根節點不能為merge?”,先看inflateViewNoAdd方法

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

重點看第8行,在inflate加載的時候檢查到mLayoutResource布局根結點含有merge就會拋出InflateException異常。這實際牽涉到LayoutInflate源碼分析,這里就不再解釋太深入了。我們只要知道ViewStub引用的布局根結點不能使用merge標簽就OK了。
  接著解決第3個問題“為什么ViewStub的inflate方法連續調用兩次就拋出異常?”。先看看replaceSelfWithView源碼

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

在第2行,首先查找該視圖(也就是ViewStub)在父節點(LinearLayout)的位置(下標)。接著從父節點中刪除該視圖。在6~8行將view的視圖添加到父節點(LinearLayout)中。
  當我們第一次調用inflate方法,總結起來就是下面這些步驟
1、先是通過ViewStub獲取它的父節點
2、尋找ViewStub在父節點中的位置,并且使用index記錄下來。
3、把ViewStub從父節點中刪除
4、將view(加載的布局)放進父節點的index位置。
  當我們第二次調用inflate方法,這是由于ViewStub已經從父節點中刪除,此時在第1步驟,獲取父節點是一個空指針,因此會直接拋出一個IllegalStateException異常。
  最后一個問題“inflate方法和setVisibility方法有什么區別?”,要回答這個問題就分析它們的源碼,由于inflate的源碼已經分析過,那就看看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();
            }
        }
    }

在2行看到一個很熟悉的變量mInflatedViewRef,這個就是剛才說過的弱引用對象,它里面存放了實例化的view對象。如果它為空就會執行第12行的infalte方法。不為空走3~8行代碼,首先從弱引用對象中拿出view的實例,接著判斷是否為空,如果為空則拋出IllegalStateException異常。不為空就按照傳進來的visibility變量對view設置顯示隱藏。
  總結:inflate只是用于布局資源,而setVisibility用于設置布局資源的可見性(顯示或隱藏)內部幫你自動加載了布局資源。
要注意的是,當你調用了setVisibility(內部調用了inflate)之后就不能再調用inflate方法。

4、結束

第一次寫源碼分析的博客,如果有不對的地方歡迎指正,謝謝(>_<)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容