理解 ViewStub 原理

本人只是Android小菜一個,寫技術文檔只是爲了總結本身在最近學習到的知識,歷來不敢爲人師,若是裏面有些不正確的地方請你們盡情指出,謝謝!java

本文基於原生Android 9.0源碼來解析 ViewStub的實現原理android

android/view/ViewStub.java
android/view/View.java
複製代碼

1. 概述

在進行Android程序開發時,除了要實現基本功能外,還要關注程序的性能,例如使用更少的內存、消耗更少的電量、更快地響應用戶操做以及更快地啓動顯示等等。這個特色註定在咱們平時工做中,有很大一部分精力都在進行性能優化,其中一個優化方向就是讓程序在儘量短的時間內啓動並顯示,讓用戶感受不到延遲,保證良好的用戶體驗。canvas

「懶加載」就是爲了讓程序儘量快地啓動而提出的一個優化策略,即讓那些對用戶不重要或者不須要當即顯示的佈局控件作延遲加載,只在須要顯示的時候才進行加載,這樣就可讓程序在啓動顯示的過程當中加載更少的控件,佔用更少的內存空間,從而更快啓動並顯示。「懶加載」策略的具體實現方式多種多樣,Android系統也提供了一種用於實現佈局控件懶加載的工具ViewStub,它可以讓相關佈局在顯示時再進行加載,從而提高程序啓動速度。性能優化

本文先簡單介紹ViewStub的使用方法,再介紹其實現「懶加載」的原理,以幫助你們加深對它的理解。app

2. ViewStub 使用方法

在講解ViewStub的使用方法前,按照慣例,咱們仍是先來看看它的聲明:框架

/** * A ViewStub is an invisible, zero-sized View that can be used to lazily inflate * layout resources at runtime. * * When a ViewStub is made visible, or when {@link #inflate()} is invoked, the layout resource * is inflated. The ViewStub then replaces itself in its parent with the inflated View or Views. * Therefore, the ViewStub exists in the view hierarchy until {@link #setVisibility(int)} or * {@link #inflate()} is invoked. * ...... */
public final class ViewStub extends View { ... }
複製代碼

從這段介紹中能夠知道ViewStub是一個不可見而且大小爲0的控件,其做用就是用來實現佈局資源的「懶加載」,當調用setVisibility()或者inflate()時,和ViewStub相關的佈局資源就會被加載並在控件層級結構中替代ViewStub,同時ViewStub會從控件層級結構中移除,再也不存在。async

如今再來看下它的使用方法,首先須要在佈局文件配置:ide

<ViewStub android:id="@+id/view_stub_id" android:layout="@layout/view_stub" android:inflatedId="@+id/view_stub_id" android:layout_width="200dp" android:layout_height="50dp" />
複製代碼

因爲ViewStub是直接繼承自View的,因此它在xml裏的基本使用方法和其餘控件是同樣的,只是有一些重要屬性須要注意,其中android:layout指的是真正須要加載的佈局資源,android:inflatedId指的是佈局資源被加載後的View ID,總結以下:函數

屬性 含義/做用 屬性級別 是否可選
android:id ViewStub 在佈局文件中的ID,用於在代碼中訪問。 View 共有 必寫
android:layout 在顯示 ViewStub 時真正加載並顯示的佈局文件 ViewStub 特有 必寫
android:inflatedId 真正佈局文件加載後建立的控件ID ViewStub 特有 可選

xml中定義ViewStub後就能夠在代碼裏直接使用並根據具體業務邏輯在須要顯示的時候對其進行加載:工具

ViewStub viewStub = (ViewStub) findViewById(R.id.view_stub_id);
if (viewStub != null) {
    // 調用 inflate 加載真正的佈局資源並返回建立的 View 對象
    View inflatedView = viewStub.inflate();
    // 在獲得真正的 View 對象後,就能夠和直接加載的控件同樣使用了。
    TextView textView = inflatedView.findViewById(R.id.view_stub_textview);
}
複製代碼

ViewStub的「懶加載」能起多大效果,取決因而否能在最合適的時機顯示它,因爲每一個模塊的業務邏輯不一樣,其最合適的顯示時機也不相同,但基本的原則就是在使用它的前一刻進行加載,這才能使ViewStub的"懶加載"做用最大化,也才能使性能最好。

3. ViewStub 原理解析

3.1 構造過程

前面在ViewStub的聲明中看到它是一個不可見且大小爲0的控件,那麼是如何作到這點的呢?首先看下它的構造過程:

public final class ViewStub extends View {
    // 在 xml 中定義的 android:inflatedId 值,用於加載後的 View Id。
    private int mInflatedId;
    // 在 xml 中定義的 android:layout 值,是須要真正加載的佈局資源。
    private int mLayoutResource;
    // 保存佈局建立的 View 弱引用,方便在 setVisibility() 函數中使用。
    private WeakReference<View> mInflatedViewRef;

    // 佈局加載器
    private LayoutInflater mInflater;
    // 佈局加載回調接口,默認爲空。
    private OnInflateListener mInflateListener;

    public ViewStub(Context context) {
        this(context, 0);
    }

    /** * Creates a new ViewStub with the specified layout resource. * * @param context The application's environment. * @param layoutResource The reference to a layout resource that will be inflated. */
    public ViewStub(Context context, @LayoutRes int layoutResource) {
        this(context, null);

        mLayoutResource = layoutResource;
    }

    public ViewStub(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ViewStub(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

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

        final TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.ViewStub, defStyleAttr, defStyleRes);
        // 獲取 xml 中定義的 android:inflatedId 和 android:layout 屬性值
        mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
        mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);
        // 獲取 ViewStub 在 xml 中定義的 id 值
        mID = a.getResourceId(R.styleable.ViewStub_id, NO_ID);
        a.recycle();

        // 設置 ViewStub 控件的顯示屬性,直接設置爲不顯示。
        setVisibility(GONE);
        // 設置 ViewStub 不進行繪製
        setWillNotDraw(true);
    }
    
    ...
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 設置寬高都爲 0
        setMeasuredDimension(0, 0);
    }

    @Override
    public void draw(Canvas canvas) {
        // 空方法,不進行任何繪製。
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
    }
}
複製代碼

ViewStub的構造過程比較簡單,相信很容易看懂,小菜也在代碼的關鍵點都增長了註釋,從中能夠發現幾點關鍵信息:

  1. ViewStub的不可見性:在ViewStub的構造函數中,利用setVisibility(GONE)將可見性設置爲不可見,因此不管在 xml裏如何設置,都是不可見的。
  2. ViewStub的不繪製性:在 ViewStub的構造函中,利用setWillNotDraw(true)使其不進行繪製而且把draw()實現爲空方法,這些都保證了ViewStub在加載的時候並不會進行實際的繪製工做。
  3. ViewStub的零大小性:在onMeasure()中把寬高都直接指定爲0,保證了其大小爲0。

正是因爲ViewStub的這些特性,其加載過程幾乎不須要時間,能夠認爲它的存在不會對相關程序的啓動產生影響。

3.2 懶加載過程

前面提到:在調用inflate()或者setVisibility()時,ViewStub纔會加載真正的佈局資源並在控件層級結構中替換爲真正的控件,同時ViewStub從控件層級結構中移除,這是「懶加載」的核心思想,是如何實現的呢?既然是調用inflate()setVisibility(),就直接分析相關代碼。

首先看下inflate()函數代碼:

/** * Inflates the layout resource identified by {@link #getLayoutResource()} * and replaces this StubbedView in its parent by the inflated layout resource. * * @return The inflated layout resource. * */
public View inflate() {
    // 獲取 ViewStub 在控件層級結構中的父控件。
    final ViewParent viewParent = getParent();

    if (viewParent != null && viewParent instanceof ViewGroup) {
        if (mLayoutResource != 0) {
            final ViewGroup parent = (ViewGroup) viewParent;
            // 根據 android:layout 指定的 mLayoutResource 加載真正的佈局資源,渲染成 View 對象。
            final View view = inflateViewNoAdd(parent);
            // 在控件層級結構中把 ViewStub 替換爲新建立的 View 對象。
            replaceSelfWithView(view, parent);

            // 保存 View 對象的弱引用,方便其餘地方使用。
            mInflatedViewRef = new WeakReference<>(view);
            // 渲染回調,默認不存在。
            if (mInflateListener != null) {
                mInflateListener.onInflate(this, view);
            }
            // 返回新建立的 View 對象
            return view;
        } else {
            // 若是沒有在 xml 指定 android:layout 會走到這個路徑,因此 android:layout 是必須指定的。
            throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
        }
    } else {
        // 在第一次調用 inflate() 後,ViewStub 會從控件層級結構中移除,再也不有父控件,
        // 此後再調用 inflate() 會走到這個路徑,因此 inflate() 只能調用一次。
        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);
    }
    // 把真正須要加載的佈局資源渲染成 View 對象。
    final View view = factory.inflate(mLayoutResource, parent, false);
    // 若是在 xml 中指定 android:inflatedId 就設置到新建立的 View 對象中,能夠不指定。
    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);

    // 把新建立的 View 對象加入控件層級結構中,而且位於 ViewStub 的位置,
    // 而且在這個過程當中,會使用 ViewStub 的佈局參數,例如寬高等。
    final ViewGroup.LayoutParams layoutParams = getLayoutParams();
    if (layoutParams != null) {
        parent.addView(view, index, layoutParams);
    } else {
        parent.addView(view, index);
    }
}
複製代碼

inflate()函數詳細地展現瞭如何渲染布局資源以及如何在控件層級結構中把ViewStub替換爲新建立的View對象,代碼比較簡單,小菜也在關鍵地方增長了註釋。

再來看下setVisibility()函數代碼:

/** * When visibility is set to {@link #VISIBLE} or {@link #INVISIBLE}, * {@link #inflate()} is invoked and this StubbedView is replaced in its parent * by the inflated layout resource. After that calls to this function are passed * through to the inflated view. * * @param visibility One of {@link #VISIBLE}, {@link #INVISIBLE}, or {@link #GONE}. * * @see #inflate() */
@Override
@android.view.RemotableViewMethod(asyncImpl = "setVisibilityAsync")
public void setVisibility(int visibility) {
    if (mInflatedViewRef != null) {
        // 若是已經調用過 inflate() 函數,mInflatedViewRef 會保存新建立 View 對象的弱引用,
        // 此時直接更新其可見性便可。
        View view = mInflatedViewRef.get();
        if (view != null) {
            view.setVisibility(visibility);
        } else {
            throw new IllegalStateException("setVisibility called on un-referenced view");
        }
    } else {
        // 若是沒有調用過 inflate() 函數就會走到這個路徑,會在設置可見性後直接調用 inflate() 函數。
        super.setVisibility(visibility);
        if (visibility == VISIBLE || visibility == INVISIBLE) {
            inflate();
        }
    }
}
複製代碼

setVisibility()的代碼邏輯也很簡單:若是inflate()已經被調用過就直接更新控件可見性,不然更新可見性並調用inflate()加載真正的佈局資源,渲染成 View 對象。

4. 總結

一些佈局控件在開始時並不須要顯示,在程序啓動後再根據業務邏輯進行顯示,一般的作法是在 xml中將其定義爲不可見,而後在代碼中經過setVisibility()更新其可見性,可是這樣作會對程序性能產生不利影響,由於雖然該控件的初始狀態是不可見,但仍然會在程序啓動時進行建立和繪製,增長了程序的啓動時間。正是因爲這種狀況的存在,Android系統提供了ViewStub框架,可以很容易實現「懶加載」以提高程序性能,本文從「使用方法」和「實現原理」兩個方面對其進行講解,但願能對你們有所幫助。

相關文章
相關標籤/搜索