探討Viewpager下的Fragment懶加載(含嵌套)

現在很多app的架構,基本都是viewpager+fragment的,復雜一點的可能還含有嵌套,例如我們公司的app:


首頁.jpg.png

可以看到采用的是:底部按鈕+[viewpager+fragment[viewpager+fragment]]嵌套的模式
簡單來講如圖:


結構.png

布局如下:

main_activity.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".lazy.ViewPagerActivity">

    <androidx.viewpager.widget.ViewPager
        android:id="@+id/view_pager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="48dp"
        android:orientation="horizontal">

        <TextView
            android:id="@+id/tv_one"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:background="@color/colorPrimary"
            android:gravity="center"
            android:text="第一頁"
            android:textColor="#ffffff"
            android:textSize="15sp" />

        <TextView
            android:id="@+id/tv_two"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:background="@color/colorPrimaryDark"
            android:gravity="center"
            android:text="第二頁"
            android:textColor="#ffffff"
            android:textSize="15sp" />

        <TextView
            android:id="@+id/tv_three"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:background="#ff0000"
            android:gravity="center"
            android:text="第三頁"
            android:textColor="#ffffff"
            android:textSize="15sp" />

        <TextView
            android:id="@+id/tv_four"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:background="#000000"
            android:gravity="center"
            android:text="第四頁"
            android:textColor="#ffffff"
            android:textSize="15sp" />
    </LinearLayout>
</LinearLayout>

fragment_a.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <TextView
            android:id="@+id/tv_a"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:background="@color/colorPrimary"
            android:gravity="center"
            android:padding="5dp"
            android:text="A"
            android:textColor="#ffffff"
            android:textSize="12sp" />

        <TextView
            android:id="@+id/tv_b"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:background="@color/colorPrimary"
            android:gravity="center"
            android:padding="5dp"
            android:text="B"
            android:textColor="#ffffff"
            android:textSize="12sp" />

        <TextView
            android:id="@+id/tv_c"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:background="@color/colorPrimary"
            android:gravity="center"
            android:padding="5dp"
            android:text="C"
            android:textColor="#ffffff"
            android:textSize="12sp" />

        <TextView
            android:id="@+id/tv_d"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:background="@color/colorPrimary"
            android:gravity="center"
            android:padding="5dp"
            android:text="D"
            android:textColor="#ffffff"
            android:textSize="12sp" />
    </LinearLayout>

    <androidx.viewpager.widget.ViewPager
        android:id="@+id/view_pager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />
</LinearLayout>

當我們深入viewpager加載需要一個adapter
android提供的有FragmentPagerAdapter/FragmentStatePagerAdapter
區別在于前者在沒有在destroyItem中調用實務的detach方法:

FragmentPagerAdapter:

@Override
    public void destroyItem(ViewGroup container, int position, Object object) {
         mCurTransaction.detach((Fragment)object);
    }

FragmentStatePagerAdapter:

@Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        Fragment fragment = (Fragment) object;
        //復制數組保存的fragment為null,不采用remove是因為在滑動的時候需要創建(instantiateItem:mFragments.set(position, fragment)),這里節省數組的插入位移數據時間來提升效率
        mFragments.set(position, null);
        //移除
        mCurTransaction.remove(fragment);
    }

這里我們主要結合FragmentStatePagerAdapter來優化viewpager下的數據加載

簡單分析FragmentStatePagerAdapter

入口viewpage.setAdapter(xxx)

public void setAdapter(@Nullable PagerAdapter adapter) {
      if (mAdapter != null) {
          //recycler adapter,將設置的adapter數據全清空
           ......
        }
    //將mAdpter復制
     mAdapter = adapter;
if (mAdapter != null) {
           //初始化adapter的一些必要參數
          .....
            if (mRestoredCurItem >= 0) {
            //還原adapter數據狀態
            ...
            } else if (!wasFirstLayout) {
            //注意這里
                populate();
            } else {
                requestLayout();
            }
        }
}

觀察populate函數,該函數是執行fragment生命周期的關鍵函數

void populate(int newCurrentItem) {
    //FragmentStatePagerAdapter的startUpdate
    mAdapter.startUpdate(this);
    if (curItem == null && N > 0) {
  
            curItem = addNewItem(mCurItem, curIndex);
        }
    //執行的是,返回一個fragment對象
    mAdapter.instantiateItem(this, position)
    //當前的fragment被選中
    mAdapter.setPrimaryItem(this, mCurItem, curItem.object);
    //完成
    mAdapter.finishUpdate(this);
}

具體可知,viewpager在調用setAdapter(FragmentStatePagerAdapter fpg)的時候,
FragmentStatePagerAdapter 會依次執行
startUpdate--->instantiateItem()-->setPrimaryItem-->finishUpdate方法,那么再看下
FragmentStatePagerAdapter 的源碼

FragmentStatePagerAdapter

startUpdate是個空實現

instantiateItem

 @Override
    public Object instantiateItem(ViewGroup container, int position) {
       //取出緩存的Fragment 
        if (mFragments.size() > position) {
            Fragment f = mFragments.get(position);
            if (f != null) {
                return f;
            }
        }
      //創建事務
        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
        Fragment fragment = getItem(position);
        //更新保存的狀態
        if (mSavedState.size() > position) {
            Fragment.SavedState fss = mSavedState.get(position);
            if (fss != null) {
                fragment.setInitialSavedState(fss);
            }
        }
        while (mFragments.size() <= position) {
            mFragments.add(null);
        }
        fragment.setMenuVisibility(false);
        //調用fragment setUserVisibleHint
        fragment.setUserVisibleHint(false);
        //更新該fragment,這里和后面set(position,null)相對應,節省性能
        mFragments.set(position, fragment);
        //執行事務的add方法,將fragment加入到事務中
        mCurTransaction.add(container.getId(), fragment);
        return fragment;
    }

setPrimaryItem

  @Override
    @SuppressWarnings("ReferenceEquality")
    public void setPrimaryItem(ViewGroup container, int position, Object object) {
        Fragment fragment = (Fragment)object;
        //當緩存的CurrentFragment不是選中的fragment執行
        if (fragment != mCurrentPrimaryItem) {
            if (mCurrentPrimaryItem != null) {
                mCurrentPrimaryItem.setMenuVisibility(false);
                //調用setUserVisibleHint
                mCurrentPrimaryItem.setUserVisibleHint(false);
            }
            if (fragment != null) {
                fragment.setMenuVisibility(true);
                //調用setUserVisibleHint
                fragment.setUserVisibleHint(true);
            }
          //更新緩存當前fragment
            mCurrentPrimaryItem = fragment;
        }
    }

destroyItem

@Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        Fragment fragment = (Fragment) object;
        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
        while (mSavedState.size() <= position) {
            mSavedState.add(null);
        }
        mSavedState.set(position, fragment.isAdded()
                ? mFragmentManager.saveFragmentInstanceState(fragment) : null);
        //set null,對應上面的add position
        mFragments.set(position, null);
        //從事務中已移除
        mCurTransaction.remove(fragment);
    }

可以看到,這里完成了fragment的生命周期,但是由于fragment的生命周期是又FragmentManager的事務FragmentTransaction來管理的,所以,在沒有調用commit之前是不會進行生命周期的
這里finishUpdate函數調用的commit

finishUpdate

@Override
    public void finishUpdate(ViewGroup container) {
        if (mCurTransaction != null) {
            mCurTransaction.commitNowAllowingStateLoss();
            mCurTransaction = null;
        }
    }

所以,在所有的生命周期中,setUserVisibleHint將會最先執行。
懶加載的本質是一種算法,因為viewpager中的預加載是無法避免的(內部緩存了mFragments來存儲fragment),在populate中兩個for循環中緩存了左右兩邊數據

 for (int pos = mCurItem - 1; pos >= 0; pos--) {
                //leftItem
                ii(left) = mItems.get(itemIndex) ;
            }
 for (int pos = mCurItem - 1; pos >= 0; pos--) {
                //leftItem
               ii(right)= mItems.get(itemIndex) ;
            }

所以,懶加載就是在數據上做優化,可見時加載數據,不可見時停止加載數據

LazyFragment

/**
 * @author chenke
 * @create 2021/2/25
 * @Describe
 */
public abstract class LazyFragment extends Fragment {
    private final String TAG = "lazy_fragment";
    private View mRootView;

    private boolean viewCreated = false;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        if (mRootView == null) {
            mRootView = inflater.inflate(getLayout(), container, false);
        }
        initView(mRootView);
        viewCreated = true;
        Log.i(TAG, getClass().getSimpleName() + "====>onCreateView");
        if (getUserVisibleHint()) {
          //由于onCreateView,在執行之前setUserVisibleHint已經執行,所以這里手動分發一次可見狀態為true
            setUserVisibleHint(true);
        }
        return mRootView;
    }

    public abstract int getLayout();

    public abstract void initView(View view);
    /**
      *setUserVisibleHint會優先于所有生命周期的執行,
      *所以這里增加標志位viewCreated,視圖創建了才執行函數
      */
    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        Log.i(TAG, getClass().getSimpleName() + "====>setUserVisibleHint");
        if (viewCreated) {
            if (isVisibleToUser) {
              //選中的時候可見,分發
                dispatchUserVisibleStatus(true);
            } else if (!isVisibleToUser) {
              //分發不可見
                dispatchUserVisibleStatus(false);
            }
        }
    }

    public void dispatchUserVisibleStatus(boolean isUserVisibleStatus) {
    
        if (isUserVisibleStatus) {
            onStartLoad();
        } else {
            onStopLoad();
        }
    }

    @Override
    public void onResume() {
        super.onResume();
        Log.i(TAG, getClass().getSimpleName() + "===>onResume");
    }

    @Override
    public void onPause() {
        super.onPause();
        Log.i(TAG, getClass().getSimpleName() + "===>onPause");
    }

    @Override
    public void onDetach() {
        super.onDetach();
        Log.i(TAG, getClass().getSimpleName() + "===>onDetach");
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        Log.i(TAG, getClass().getSimpleName() + "===>onDestroyView");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.i(TAG, getClass().getSimpleName() + "===>onDestroy");
    }

    /**
     * 子類重寫該方法來實現開始加載數據
     */
    public void onStartLoad() {
        Log.i(TAG, getClass().getSimpleName() + "====>開始加載數據onStartLoad");
    }

    /**
     * 子類重寫該方法來實現暫停數據加載
     */
    public void onStopLoad() {
        Log.i(TAG, getClass().getSimpleName() + "====>停止加載數據onStopLoad");
    }
}

如圖:


下飯.gif

日志輸出情況:


生命周期調用.png

可以看到符合預期,那么這里是不是結束了,顯然不是,因為在viewpage+fragment中如果存在嵌套的情況下,他在滑動到,嵌套的fragment,那么fragment的子類都會執行
嵌套gif.gif

嵌套.png

所以需要加上在嵌套的時候,父類去控制子類的分發

public void dispatchUserVisibleStatus(boolean isUserVisibleStatus) {
        if (isUserVisibleStatus) {
            onStartLoad();
        } else {
            onStopLoad();
        }
        //在嵌套模式下,讓子類的fragment進行分發
        FragmentManager fm = getChildFragmentManager();
        List<Fragment> fragments = fm.getFragments();
        if (fragments.size() > 0) {
            for (Fragment fragment : fragments) {
                if (fragment instanceof LazyFragment) {
                    //當前的fragment狀態為可見時才分發
                    if (fragment.getUserVisibleHint()) {
                        ((LazyFragment) fragment).dispatchUserVisibleStatus(true);
                    }
                }
            }
        }
    }
攔截過后的生命周期.png

但,此時的懶加載還不完整,因為,當如果我們從fragment跳轉到一個activity過后,由于viewpager沒有做改變,所以不會觸發setUserVisibleHint函數,我們需要在onResume和onPause來執行數據的暫停和加載

/**
  *記錄當前fragment在執行數據加載/停止加載之前的狀態
  */
boolean currentVisibleStatus=false
 @Override
    public void onResume() {
        super.onResume();
        Log.i(TAG, getClass().getSimpleName() + "===>onResume");
        if (getUserVisibleHint() && !currentVisibleStatus) {
          //getUserVisibleHint() 不會執行,始終為true
          //不可見-->可見,加載數據
            dispatchUserVisibleStatus(true);
        }
    }

    @Override
    public void onPause() {
        super.onPause();
        Log.i(TAG, getClass().getSimpleName() + "===>onPause");
        if (getUserVisibleHint() && currentVisibleStatus) {
         //可見-->不可見,停止加載
            dispatchUserVisibleStatus(false);
        }
    }
 @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        Log.i(TAG, getClass().getSimpleName() + "====>setUserVisibleHint");
        if (viewCreated) {
            if (!currentVisibleStatus && isVisibleToUser) {
              //之前狀態不可見-->當前狀態可見,加載數據
                dispatchUserVisibleStatus(true);
            } else if (currentVisibleStatus && !isVisibleToUser) {
              //之前狀態可見-->當前狀態不可見,暫停數據加載
                dispatchUserVisibleStatus(false);
            }
        }
    }

完整LazyFragmet

/**
 * @author chenke
 * @create 2021/2/25
 * @Describe
 */
public abstract class LazyFragment extends Fragment {
    private final String TAG = "lazy_fragment";
    private View mRootView;

    private boolean viewCreated = false;

    private boolean currentVisibleStatus = false;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        if (mRootView == null) {
            mRootView = inflater.inflate(getLayout(), container, false);
        }
        initView(mRootView);
        viewCreated = true;
        Log.i(TAG, getClass().getSimpleName() + "====>onCreateView");
        if (getUserVisibleHint()) {
            setUserVisibleHint(true);
        }
        return mRootView;
    }

    public abstract int getLayout();

    public abstract void initView(View view);

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        Log.i(TAG, getClass().getSimpleName() + "====>setUserVisibleHint");
        if (viewCreated) {
            if (!currentVisibleStatus && isVisibleToUser) {
                dispatchUserVisibleStatus(true);
            } else if (currentVisibleStatus && !isVisibleToUser) {
                dispatchUserVisibleStatus(false);
            }
        }
    }

    public void dispatchUserVisibleStatus(boolean isUserVisibleStatus) {
        currentVisibleStatus = isUserVisibleStatus;
        if (isUserVisibleStatus) {
            onStartLoad();
        } else {
            onStopLoad();
        }
        //在嵌套模式下,讓子類的fragment進行分發
        FragmentManager fm = getChildFragmentManager();
        List<Fragment> fragments = fm.getFragments();
        if (fragments.size() > 0) {
            for (Fragment fragment : fragments) {
                if (fragment instanceof LazyFragment) {
                    if (fragment.getUserVisibleHint()) {
                        ((LazyFragment) fragment).dispatchUserVisibleStatus(true);
                    }
                }
            }
        }
    }

    @Override
    public void onResume() {
        super.onResume();
        Log.i(TAG, getClass().getSimpleName() + "===>onResume");
        if (getUserVisibleHint() && !currentVisibleStatus) {
            dispatchUserVisibleStatus(true);
        }
    }

    @Override
    public void onPause() {
        super.onPause();
        Log.i(TAG, getClass().getSimpleName() + "===>onPause");
        if (getUserVisibleHint() && currentVisibleStatus) {
            dispatchUserVisibleStatus(false);
        }
    }

    @Override
    public void onDetach() {
        super.onDetach();
        Log.i(TAG, getClass().getSimpleName() + "===>onDetach");
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        Log.i(TAG, getClass().getSimpleName() + "===>onDestroyView");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.i(TAG, getClass().getSimpleName() + "===>onDestroy");
    }

    /**
     * 子類重寫該方法來實現開始加載數據
     */
    public void onStartLoad() {
        Log.i(TAG, getClass().getSimpleName() + "====>開始加載數據onStartLoad");
    }

    /**
     * 子類重寫該方法來實現暫停數據加載
     */
    public void onStopLoad() {
        Log.i(TAG, getClass().getSimpleName() + "====>停止加載數據onStopLoad");
    }
}

總結:

  • viewpager+fragment的結構下,子fragment集合會依次執行startUpdate--->instantiateItem()-->setPrimaryItem-->finishUpdate
  • 由于fragment的由FragmentManager的事務FragmentTransaction來管理,而viewpager是在finishUpdate函數后才提交事務,所以setUserVisibleHinit會最先執行。
  • FragmentStatePagerAdapter和FragmentPagerAdapter區別在于前者調用事務的remove函數,而后者這是detach,所有FragmentStatePagerAdapter需要幾率fragment的狀態,更消耗內存
  • viewpager內部存儲的mFragments數組是通過置空來優化list的性能(add和remove會頻繁的移動list的指針)

以上是分析support包下的viewpager+fragment的懶加載,由于事務在是setUserVisibleHint之后執行,讓viewpager+fragment的這種結構的數據,在可見情況下無法做到有效的控制,所以AndroidX下的Fragment新增的maxLifecycle來控制最大生命周期,這也是setUserVisibleHint被廢棄的原因,下面我會再介紹在AndroidX下如何通過最大生命周期來優化懶加載。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容