天天看點

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