ViewStub源碼分析

  ViewStub是一種特殊的View,Android官方給出的解釋是:一種不可見的(GONE)、size是0的佔位view,多用於運行時html

延遲加載的,也就是說真正須要某個view的時候。在實際項目中,我發現它試用的場景大致有2種:android

1. 某種只第一次須要顯示的view,好比某個介紹性的東西,好比用戶觸發了某些操做,給他彈出一個使用介紹,canvas

這種介紹只出現一次,以後的操做中不會再出現,因此這種狀況下能夠先用ViewStub來佔個位,等介紹的view真正服務器

須要的時候才inflate,通常狀況下這種view的建立也比較expensive;less

2. 某種直到運行時某一刻的時候才知道應該加載哪一個view,好比你可能須要根據服務器返回的某些數據來決定是顯示View 1ide

仍是顯示View 2,這種狀況下,也能夠先用ViewStub佔位,直到能夠作決定了在inflate相應的view;佈局

爲了接下來的分析方便,這裏我先貼一段ViewStub的xml代碼,以下:性能

<ViewStub 
    android:id="@+id/stub"
    android:inflatedId="@+id/realView" // 可設置,不過通常意義不大
    android:layout="@layout/my_real_view"
    android:visibility="GONE" // really no need, don't write useless code
    android:layout_width="match_parent"
    android:layout_height="40dip" />

在代碼裏你一樣能夠經過findViewById(R.id.stub)來找到此ViewStub,當真正的"my_real_view"佈局被inflate以後,優化

ViewStub這個控件就被從view層次結構中刪除了,取而代之的就是"my_real_view"表明的佈局。注意下,這裏的全部this

layout_width/height都是用來控制inflate以後的view的,而不是給ViewStub用的,這個多是Android系統裏惟一的例外。

在咱們的Java代碼裏使用的方式,官方推薦的作法是:

ViewStub stub = (ViewStub) findViewById(R.id.stub);
View inflated = stub.inflate();

但這裏有一點須要留意的就是findViewById(R.id.stub),由於咱們知道stub在第一次被inflate以後就會從view層次結構中

刪除,因此你應該要確保這樣的findViewById只會被調用一次(通常都是這樣的狀況),但在有些你沒想到的case下

可能也會被調用屢次。前幾天咱們在項目裏就遇到了這個狀況,出現了NPE,最終調查發現是stub的findViewById被又調用

了一次,致使了NPE(由於層次結構中已經找不到這個id了)。好的作法是好比把這種findViewById的放在onCreate相似

的只會被執行一次的地方,不然你就應該格外當心,本身加些保護處理。

  說了這麼多,接下來能夠切入正題了,廢話很少說,從ctor開始上代碼:

    public ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        TypedArray a = context.obtainStyledAttributes(
                attrs, com.android.internal.R.styleable.ViewStub, defStyleAttr, defStyleRes);

        mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID); // 從layout文件中讀取inflateId的值,若是有的話
        mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0); // 一樣的,讀取真正的layout文件

        a.recycle();

        a = context.obtainStyledAttributes(
                attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
        mID = a.getResourceId(R.styleable.View_id, NO_ID); // 讀取View_id屬性
        a.recycle();

        initialize(context);
    }

    private void initialize(Context context) {
        mContext = context;
        setVisibility(GONE); // 看到這裏,你就明白了我前面xml文件中提到的不必手動指定android:visibility="GONE"的緣由
        setWillNotDraw(true); // 由於ViewStub只是個臨時佔位的,它不作任何繪製操做,因此打開這個標誌位以優化繪製過程,提升性能
    }

  接下來,咱們看幾個相關的比較簡單的方法,以下:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(0, 0); // 實現很簡單,寫死的大小爲0,0
    }

    @Override
    public void draw(Canvas canvas) { // 不作任何繪製
    }

    @Override
    protected void dispatchDraw(Canvas canvas) { // 沒啥須要dispatch的
    }

    /**
     * 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
    public void setVisibility(int visibility) { // setVisibility作了些特殊處理
        if (mInflatedViewRef != null) { // 真正View的弱引用
            View view = mInflatedViewRef.get();
            if (view != null) {
                view.setVisibility(visibility);
            } else {
                throw new IllegalStateException("setVisibility called on un-referenced view");
            }
        } else { // 當還沒inflate的時候,若是調了setVisibility方法,若是不是GONE的狀況會自動在這種狀況下調inflate的
            super.setVisibility(visibility);
            if (visibility == VISIBLE || visibility == INVISIBLE) {
                inflate(); // 非GONE的狀況,會多調用inflate()方法,因此你拿到ViewStub的實例後既能夠主動調
            }              // inflate()方法也能夠經過setVisiblity(VISIBLE/INVISIBLE)來觸發對inflate的間接調用
        }
    }

  最後,咱們來看看真正關鍵的方法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() {
        final ViewParent viewParent = getParent();

        if (viewParent != null && viewParent instanceof ViewGroup) {
            if (mLayoutResource != 0) {
                final ViewGroup parent = (ViewGroup) viewParent;
                final LayoutInflater factory;
                if (mInflater != null) {
                    factory = mInflater;
                } else {
                    factory = LayoutInflater.from(mContext);
                }
                final View view = factory.inflate(mLayoutResource, parent,
                        false); // 注意這行,實際上仍是調用了LayoutInflater的inflate方法,注意最後一個參數是false,因此這裏返回的
                                // view就是真正layout文件裏的root view而不是parent了。
                if (mInflatedId != NO_ID) {
                    view.setId(mInflatedId); // 若是layout文件裏有給真正的view指定id就在這裏設置上
                }

                final int index = parent.indexOfChild(this);
                parent.removeViewInLayout(this); // 找到ViewStub在parent中的位置並刪除ViewStub,看到了,在inflate的過程當中
                                                 // ViewStub真的是被刪掉了!!!
                final ViewGroup.LayoutParams layoutParams = getLayoutParams(); // 獲取LayoutParams,就是你在xml文件中寫的
// layout_xxx這樣的屬性,雖然其實是屬於ViewStub的,
if (layoutParams != null) { // 但下面緊接着就用這個LayoutParams來添加真正的view了 parent.addView(view, index, layoutParams); // 注意下添加的位置就是原先ViewStub待的位置,
// 因此等於就是完徹底全替換了 }
else { parent.addView(view, index); // 若是layoutParams是null的case } mInflatedViewRef = new WeakReference<View>(view); // 用view初始化弱引用 if (mInflateListener != null) { // 經過mInflateListener將inflate完成事件通知出去。。。 mInflateListener.onInflate(this, view); } return view; // 返回真正的view } else { // ViewStub必須指定真正的layout文件!!! throw new IllegalArgumentException("ViewStub must have a valid layoutResource"); } } else { // ViewStub也必須有個parent view的!!! throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent"); } } /** * Specifies the inflate listener to be notified after this ViewStub successfully * inflated its layout resource. * * @param inflateListener The OnInflateListener to notify of successful inflation. * * @see android.view.ViewStub.OnInflateListener */ public void setOnInflateListener(OnInflateListener inflateListener) { mInflateListener = inflateListener; // 技術上要想響應inflate完成事件,有2種途徑,這裏經過設置listener是一種方式, } // 也能夠經過更generic的方式:override真正view的onFinishInflate方法。 /** * Listener used to receive a notification after a ViewStub has successfully * inflated its layout resource. * * @see android.view.ViewStub#setOnInflateListener(android.view.ViewStub.OnInflateListener) */ public static interface OnInflateListener { /** * Invoked after a ViewStub successfully inflated its layout resource. * This method is invoked after the inflated view was added to the * hierarchy but before the layout pass. * * @param stub The ViewStub that initiated the inflation. * @param inflated The inflated View. */ void onInflate(ViewStub stub, View inflated); } }

  ViewStub的使用也被Android歸到提升性能裏面,之因此這麼說一方面是由於ViewStub有延遲加載(直到須要的時候才inflate

真正的view)、先佔位的做用,另一方面也是因爲對measure、layout、draw這3個關鍵過程的優化,由於ViewStub的這3個過程

很簡單,或者根本就是do nothing,因此性能很好。綜上,Android才建議咱們用ViewStub來優化layout性能,固然也是真正合適使用

ViewStub的時機,而不是要你在全部的layout文件中都這麼作,這個時機在開始的時候我也提到了,另外也能夠參考下Android官方的文章:

http://developer.android.com/training/improving-layouts/loading-ondemand.html

http://developer.android.com/reference/android/view/ViewStub.html

 

P.S. 這篇文章原本應該是在上週末完成的,這週末去千島湖outing,在酒店裏實在無聊,發現還有篇未完成的文章就在這裏補充下,enjoy。。。 

相關文章
相關標籤/搜索