Android Fragment + ViewPager的懶加載實現

概述

Android日常開發中除了四個組件之外,還有一種使用頻率很高的組件——Fragment。在使用時我們通常需要在Fragment的各種生命週期方法中處理數據加載、頁面刷新和資源釋放等邏輯操作。

但是當Fragment遇上了ViewPager,事情就變得有點不一樣了。Fragment的生命週期變得不再那麼可控,當顯示Fragment A時,相鄰的Fragment B的一些生命週期方法也會觸發。這是因爲ViewPager爲了優化切換效果,使切換更流暢、順滑。引入了預加載和緩存機制,通常會預加載前一個和後一個Fragment,讓前一個和後一個Fragment提前初始化。

當頁面佈局過於複雜或者數據量比較大,甚至當Fragment中有播放器時,預加載會耗費資源,造成頁面卡頓甚至頁面播放器出現異常報錯。

懶加載

使用懶加載的意義就在於只有當Fragment被顯示時,纔會去加載耗費資源的素材和數據,可以節省資源、提升頁面流暢度,而且讓流程變得更可控。

實現思路

Fragment中提供了一對可見性相關的方法setUserVisibleHint(boolean isVisibleToUser)getUserVisibleHint()可以通過重寫setUserVisibleHint()來監聽頁面可見性變化,當頁面從不可見變爲可見時觸發加載數據方法,反之也可以實現頁面從可見到不可見時部分資源的釋放操作。

實現

先實現一個Fragment + ViewPager的結構(實現很簡單省略了),依次有三個Fragment爲:AFragment、BFragment和CFragemtn,三個Fragment分別繼承基類BaseLazyLoadFragment。

生命週期變化

在基類中添加生命週期方法的打印,如下圖:

從Fragment的生命週期變化可以看出,需要注意的有幾點:

  • setUserVisibleHint()方法的調用在onCreateView()方法之前。
  • 進入Activity時第一個被顯示的Fragment,會調用兩次setUserVisibleHint()第一次值爲false,第二次值爲true。
  • ViewPager的預加載會讓還沒顯示的Fragment提前初始化。
  • 當AFragment切換到BFragment時,會先調用AFragment的setUserVisibleHint(false)方法,後調用BFragment的setUserVisibleHint(true),我們可以在AFragment中做部分資源的釋放操作。
  • 當BFragment切換到AFragment時,AFragment會執行onDestroyView()方法釋放持有的佈局資源,但是AFragment中的數據資源並沒有釋放。
  • 當從CFragment切換回BFragment時,AFragment會重新初始化。

代碼實現

基於以上幾點問題,我們通過來通過代碼實現BaseLazyLoadFragment。

public abstract class BaseLazyLoadFragment extends Fragment {
    protected String TAG = BaseLazyLoadFragment.class.getSimpleName();

    //Root View
    protected View view;

    //佈局是否初始化完成
    private boolean isLayoutInitialized = false;
    //懶加載完成
    private boolean isLazyLoadFinished = false;
    //記錄頁面可見性
    private boolean isVisibleToUser = false;
    //不可見時釋放部分資源
    private boolean isInVisibleRelease = false;

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, getClass().getSimpleName() + "  onCreate");
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        Log.d(TAG, getClass().getSimpleName() + "  onCreateView");
        view = inflater.inflate(initLayout(),null);

        initView();

        return view;
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        Log.d(TAG, getClass().getSimpleName() + "  onDestroyView");

        //頁面釋放後,重置佈局初始化狀態變量
        isLayoutInitialized = false;
        this.view = null;
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        Log.d(TAG, getClass().getSimpleName() + "  onActivityCreated");
        //此方法是在第一次初始化時onCreateView之後觸發的
        //onCreateView和onActivityCreated中分別應該初始化哪些數據可以參考:
        //https://stackoverflow.com/questions/8041206/android-fragment-oncreateview-vs-onactivitycreated

        isLayoutInitialized = true;
        //第一次初始化後需要處理一次可見性事件
        //因爲第一次初始化時setUserVisibleHint方法的觸發要先於onCreateView
        dispatchVisibleEvent();
    }

    @Override
    public void onStart() {
        super.onStart();
        Log.d(TAG, getClass().getSimpleName() + "  onStart");
    }

    @Override
    public void onResume() {
        super.onResume();
        Log.d(TAG, getClass().getSimpleName() + "  onResume");

        //頁面從其他Activity返回時,重新加載被釋放的資源
        if(isLazyLoadFinished && isLayoutInitialized && isInVisibleRelease){
            visibleReLoad();

            isInVisibleRelease = false;
        }
    }

    @Override
    public void onPause() {
        super.onPause();
        Log.d(TAG, getClass().getSimpleName() + "  onPause");

        //當從Fragment切換到其他Activity釋放部分資源
        if(isLazyLoadFinished && isVisibleToUser){
            //頁面從可見切換到不可見時觸發,可以釋放部分資源,配合reload方法再次進入頁面時加載
            inVisibleRelease();

            isInVisibleRelease = true;
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d(TAG, getClass().getSimpleName() + "  onDestroy");

        //重置所有數據
        this.view = null;
        isLayoutInitialized = false;
        isLazyLoadFinished = false;
        isVisibleToUser = false;
        isInVisibleRelease = false;
    }

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        Log.d(TAG, getClass().getSimpleName() + "  setUserVisibleHint isVisibleToUser = " + isVisibleToUser);

        dispatchVisibleEvent();
    }

    /**
     * 處理可見性事件
     */
    private void dispatchVisibleEvent(){
        Log.d(TAG, getClass().getSimpleName() + "  dispatchVisibleEvent isVisibleToUser = " + getUserVisibleHint()
                + " --- isLayoutInitialized = " + isLayoutInitialized + " --- isLazyLoadFinished = " + isLazyLoadFinished);

        if(getUserVisibleHint() && isLayoutInitialized){
            //可見
            if(!isLazyLoadFinished){
                //第一次可見,懶加載
                lazyLoad();
                isLazyLoadFinished = true;
            } else{
                //非第一次可見,刷新數據
                visibleReLoad();
            }
        } else{
            if(isLazyLoadFinished && isVisibleToUser){
                //頁面從可見切換到不可見時觸發,可以釋放部分資源,配合reload方法再次進入頁面時加載
                inVisibleRelease();
            }
        }

        //處理完可見性事件之後修改isVisibleToUser狀態
        this.isVisibleToUser = getUserVisibleHint();
    }

    /**
     * 初始化View
     */
    protected abstract void initView();

    /**
     * 綁定佈局
     * @return 佈局ID
     */
    protected abstract int initLayout();

    /**
     * 懶加載<br/>
     * 只會在初始化後第一次可見時調用一次。
     */
    protected abstract void lazyLoad();

    /**
     * 刷新數據加載<br/>
     * 配合{@link #lazyLoad()},在頁面非第一次可見時刷新數據
     */
    protected abstract void visibleReLoad();

    /**
     * 當頁面從可見變爲不可見時,釋放部分數據和資源。<br/>
     * 比如頁面播放器的釋放或者一些特別佔資源的數據的釋放
     */
    protected abstract void inVisibleRelease();
}

代碼註釋比較詳細了,簡單說一下。BaseLazyLoadFragment中提供了

  • lazyLoad()方法當頁面被顯示時做懶加載;
  • visibleReLoad()方法當頁面沒有被釋放且從不可見狀態切換到可見時刷新數據用;
  • inVisibleRelease()方法當頁面從可見狀態切換到不可見時,做部分資源釋放(如播放器等)。
  • 同樣支持當切換到其他Activity時,觸發inVisibleRelease()方法做資源釋放,從Activity返回頁面時觸發visibleReLoad()刷新加載數據。

小結

以上封裝的BaseLazyLoadFragment應該能夠滿足Fragment + ViewPager實現方式的大多數需求場景。

針對Fragment + ViewPager的懶加載實現,還有一種實現方式:從ViewPager上入手,既然是因爲ViewPager的預加載導致的Fragment的生命週期不可控,那麼關掉ViewPager的預加載就好了。這種實現方式需要重寫ViewPager,需要閱讀ViewPager源碼針對預加載部分進行修改,而且在不同SDK版本的ViewPager的具體邏輯有差異,只能對某一版本的ViewPager進行修改。

至於那種實現方式更合適,那就需要按具體需求分析了。我個人比較推薦BaseLazyLoadFragment的實現方式,實現簡單、適配性更好也更加優雅。

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