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

現(xiàn)在很多app的架構(gòu),基本都是viewpager+fragment的,復(fù)雜一點的可能還含有嵌套,例如我們公司的app:


首頁.jpg.png

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


結(jié)構(gòu).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
區(qū)別在于前者在沒有在destroyItem中調(diào)用實務(wù)的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;
        //復(fù)制數(shù)組保存的fragment為null,不采用remove是因為在滑動的時候需要創(chuàng)建(instantiateItem:mFragments.set(position, fragment)),這里節(jié)省數(shù)組的插入位移數(shù)據(jù)時間來提升效率
        mFragments.set(position, null);
        //移除
        mCurTransaction.remove(fragment);
    }

這里我們主要結(jié)合FragmentStatePagerAdapter來優(yōu)化viewpager下的數(shù)據(jù)加載

簡單分析FragmentStatePagerAdapter

入口viewpage.setAdapter(xxx)

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

觀察populate函數(shù),該函數(shù)是執(zhí)行fragment生命周期的關(guān)鍵函數(shù)

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

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

FragmentStatePagerAdapter

startUpdate是個空實現(xiàn)

instantiateItem

 @Override
    public Object instantiateItem(ViewGroup container, int position) {
       //取出緩存的Fragment 
        if (mFragments.size() > position) {
            Fragment f = mFragments.get(position);
            if (f != null) {
                return f;
            }
        }
      //創(chuàng)建事務(wù)
        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
        Fragment fragment = getItem(position);
        //更新保存的狀態(tài)
        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);
        //調(diào)用fragment setUserVisibleHint
        fragment.setUserVisibleHint(false);
        //更新該fragment,這里和后面set(position,null)相對應(yīng),節(jié)省性能
        mFragments.set(position, fragment);
        //執(zhí)行事務(wù)的add方法,將fragment加入到事務(wù)中
        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執(zhí)行
        if (fragment != mCurrentPrimaryItem) {
            if (mCurrentPrimaryItem != null) {
                mCurrentPrimaryItem.setMenuVisibility(false);
                //調(diào)用setUserVisibleHint
                mCurrentPrimaryItem.setUserVisibleHint(false);
            }
            if (fragment != null) {
                fragment.setMenuVisibility(true);
                //調(diào)用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,對應(yīng)上面的add position
        mFragments.set(position, null);
        //從事務(wù)中已移除
        mCurTransaction.remove(fragment);
    }

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

finishUpdate

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

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

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

所以,懶加載就是在數(shù)據(jù)上做優(yōu)化,可見時加載數(shù)據(jù),不可見時停止加載數(shù)據(jù)

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,在執(zhí)行之前setUserVisibleHint已經(jīng)執(zhí)行,所以這里手動分發(fā)一次可見狀態(tài)為true
            setUserVisibleHint(true);
        }
        return mRootView;
    }

    public abstract int getLayout();

    public abstract void initView(View view);
    /**
      *setUserVisibleHint會優(yōu)先于所有生命周期的執(zhí)行,
      *所以這里增加標志位viewCreated,視圖創(chuàng)建了才執(zhí)行函數(shù)
      */
    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        Log.i(TAG, getClass().getSimpleName() + "====>setUserVisibleHint");
        if (viewCreated) {
            if (isVisibleToUser) {
              //選中的時候可見,分發(fā)
                dispatchUserVisibleStatus(true);
            } else if (!isVisibleToUser) {
              //分發(fā)不可見
                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");
    }

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

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

如圖:


下飯.gif

日志輸出情況:


生命周期調(diào)用.png

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

嵌套.png

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

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

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

/**
  *記錄當前fragment在執(zhí)行數(shù)據(jù)加載/停止加載之前的狀態(tài)
  */
boolean currentVisibleStatus=false
 @Override
    public void onResume() {
        super.onResume();
        Log.i(TAG, getClass().getSimpleName() + "===>onResume");
        if (getUserVisibleHint() && !currentVisibleStatus) {
          //getUserVisibleHint() 不會執(zhí)行,始終為true
          //不可見-->可見,加載數(shù)據(jù)
            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) {
              //之前狀態(tài)不可見-->當前狀態(tài)可見,加載數(shù)據(jù)
                dispatchUserVisibleStatus(true);
            } else if (currentVisibleStatus && !isVisibleToUser) {
              //之前狀態(tài)可見-->當前狀態(tài)不可見,暫停數(shù)據(jù)加載
                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進行分發(fā)
        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");
    }

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

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

總結(jié):

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容