ViewStub源碼分析

 

爲了優化UI加載,通常會把不需要立即顯示的View放到ViewStub裏,在需要的時候按需加載,以此來優化UI性能。

  • 特點

1.ViewStub 是一個輕量級的View,沒有尺寸,不繪製任何東西

2.在視圖樹中充當佔位符的作用,在需要的時候才加載真正顯示的View,實現View的延遲加載,避免資源浪費,減少渲染時間。

3.缺點是ViewStub所要替代的layout根佈局是<merge>標籤
 

  • 基本用法:

1.在佈局中直接引用ViewStub, 通過ViewStub的屬性來指定對應的layout即可,如下:

  <ViewStub
        android:id="@+id/viewstub"
        android:layout_width="552dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="65dp"
        android:layout_marginTop="570dp"
        android:layout="@layout/loading_layout" />

2.使用時,通過調用ViewStub的setVisibility()或者inflate()方法,實現加載顯示。兩者的區別,後面結合源碼分析

3.注意事項:a).layout只能加載一次  b)layout加載之後,就不能通過ViewStub的Id獲取到了。

  • 源碼分析

1.ViewStub在佈局中起到佔位符的作用,本身不顯示,不繪製,如下。

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

        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();
        //設置ViewStub不可見,不繪製
        setVisibility(GONE);
        setWillNotDraw(true);
    }

 2 . 加載指定的layout佈局並添加到ViewStub的Parent中,加載成功之後ViewStub會從它的父容器,因此無法再使用

      a) 通過inflate()方法加載layout,inflate會通過LayoutInflater加載對應資源id對應的佈局文件,

   /**
     * 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) {
            //mLayoutResource 真正要加載的佈局文件
            if (mLayoutResource != 0) {
                final ViewGroup parent = (ViewGroup) viewParent;
                final View view = inflateViewNoAdd(parent);
                //使用真正要加載的佈局替換viewStub
                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 {
            // 這裏也說明了爲什麼ViewStub不能放在merge標籤下,因爲merge不是View,
            throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
        }
    }

       加載獲取對應的View

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

       替換並移除ViewStub, 加載真正要顯示的佈局後,ViewStub便從父容器裏移除掉了,也從整個視圖樹中移除了,所findViewById找不到。

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

  b) 通過setVisibility()方法加載,在已經加載過View的情況下,會從緩存中獲取顯示View,在沒加載時會調用inflate()方法,也就是上面的加載流程

    /**
     * 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) {
            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();
            }
        }
    }

  c) ViewStub爲什麼不支持merge ?

     首先,瞭解一下merge標籤的特點:引自https://www.jianshu.com/p/69e1a3743960

  • merge必須放在佈局文件的根節點上。
  • merge並不是一個ViewGroup,也不是一個View,它相當於聲明瞭一些視圖,等待被添加。
  • merge標籤被添加到A容器下,那麼merge下的所有視圖將被添加到A容器下。
  • 因爲merge標籤並不是View,所以在通過LayoutInflate.inflate方法渲染的時候, 第二個參數必須指定一個父容器,且第三個參數必須爲true,也就是必須爲merge下的視圖指定一個父親節點
  • 因爲merge不是View,所以對merge標籤設置的所有屬性都是無效的。

    主要關注紅色加深處,merge載通過LayoutInflate.inflate方法渲染時必須指定父容器,切attachToRoot必須爲true。

    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        if (DEBUG) {
            Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                    + Integer.toHexString(resource) + ")");
        }

        final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }

回到ViewStub加載View的方法,如下,可以看到,加載佈局的attachToRoot參數默認值爲false,因此ViewStub不支持merge,當我們使用merge時會報錯:Caused by: android.view.InflateException: can be used only with a valid ViewGroup root and attachToRoot=true 。

    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;
    }
  • 典型應用

      Viewpager+Fragment的形式,在有多個Fragment的情況下,由於想要進行預加載,在首次進入某一個頁面時,雖然只有一個Fragment呈現在眼前,Viewpager下其他的Fragment的生命週期函數onCreateView(), onResume()也會執行(與setOffscreenPageLimit的設置有關),也就是說其他Fragment頁面佈局的加載,在onCreateView(), onResume()裏的邏輯也會被執行,這顯然不是很有必要。而且會增加我們想要打開的界面的加載時長。

此時就可以通過:ViewStub+setUserVisibleHint()實現佈局的懶加載以及延遲初始化,在onCreateView中只加載佈局的"殼",在真正切換到要顯示的fragment頁時,再將真正的佈局加載進來。

主要代碼如下:


 /**
 * 標誌位,標誌已經初始化完成
 */
private boolean isPrepared;

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.joy_listen_fragment, container, false);
    viewStub = view.findViewById(R.id.enjoy_viewstub);
    ...
    isPrepared = true;
    return view;
}
 
public void initVew(View view) {
    ......
}
 
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
    super.setUserVisibleHint(isVisibleToUser);
    if (isPrepared && isVisibleToUser) {
        if (viewStub.getParent() != null) {
            View view = viewStub.inflate();
            initVew(view);
            mAppsPresenterImpl = new AppsPresenterImpl(this, getActivity());
            mAppsPresenterImpl.getCacheApps(DBConstants.APPS_TYPE_JOY_LISTEN, null);
            .....
        }
    }
}

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章