ViewPager + Fragment的懒汉加载模式
首先,说下viewpager的使用场景下为什么需要懒汉式加载.
ViewPager本身是带有缓存机制的,对缓存页数的设置可以通过
public void setOffscreenPageLimit(int limit) {}实现,但是这个函数的实现中,有默认缓存页数DEFAULT_OFFSCREEN_PAGES 值为1,就是说即使你调用这个函数设置缓存页为0,实际是不会生效的,因为他会根据如果缓存页小于默认值,那么缓存页数就是默认值1.
它的缓存机制,大致原理是加载当前页时,会去缓存相邻页,这个页数是当前页两边的分别有几页,比如说设置为1,就是当前页左右两边都缓存一页. 这也是会出现懒加载机制的原因,
ViewPager的默认机制,总是会去缓存,会去预加载一页数据,这个做法原本是为了让滑动效果更流畅,在从第一页滑到第二页时,因为第二页的数据已经加载过了,所以可以立即显示出来,但是也正因为在加载第一页时,要去预加载多页,这可能拖累它所属activity的初始化速度,有可能会多次预加载,但是实际哪些页面没来得及显示,那么预加载多页也浪费了资源,流量。
所以,懒加载,就是在这个页面的UI为用户可见时才去加载数据,不在依赖预加载机制,提前加载多页数据。
然后,实现懒加载的流程:
1,懒加载的时机,界面可见的时候才去加载,这里就有界面可见,不可见的情况判断.
界面可见,又分第一次可见,非第一次可见.
怎么去判断fragment可见?是在其生命周期:onAttach -> onCreate -> onCreatedView -> onActivityCreated -> onStart -> onResume -> onPause -> onStop -> onDestroyView -> onDestroy -> onDetach.
的那个阶段呢? 答案是跟这个生命周期没关系,并不是我们印象中onResume时UI可见,onPause失去焦点,onstop不可见.
在ViewPager搭配Fragment时,我们关注的生命周期:
onCreatedView + onActivityCreated + onResume + onPause + onDestroyView
看一个从Fragment1,滑到Fragment2的log:
2020-02-12 22:35:50.643 12 myviewpager.FragmentDemo: setUserVisibleHint()--,fragment 3,isVisibleToUser=false
2020-02-12 22:35:50.643 myviewpager.FragmentDemo: setUserVisibleHint()--,fragment 1,isVisibleToUser=false
2020-02-12 22:35:50.643 myviewpager.FragmentDemo: setUserVisibleHint()--,fragment 2,isVisibleToUser=true
2020-02-12 22:35:50.647 .myviewpager.FragmentDemo: onCreateView()--, fragment 3
2020-02-12 22:35:50.648 myviewpager.FragmentDemo: onResume()--, fragment 3
从Log看,public void setUserVisibleHint(boolean isVisibleToUser)这个函数跟生命周期是无关的,它是用来表面Fragment的UI是否为用户可见,类似的还有一个函数:
public void onHiddenChanged(boolean hidden)。
Log中表明,Fragment1,Fragment3都是 false,表明对用户不可见,只有Fragment2是true,表明当前Fragment2是可见的,同时去预加载Fragment3,。
这个函数的调用堆栈:
myviewpager.FragmentDemo.setUserVisibleHint(FragmentDemo.java:40)
androidx.fragment.app.FragmentPagerAdapter.setPrimaryItem(FragmentPagerAdapter.java:138)
androidx.viewpager.widget.ViewPager.populate(ViewPager.java:1234)
androidx.viewpager.widget.ViewPager.populate(ViewPager.java:1092)
其中setPrimaryItem,就是设置当前的Fragment的可见性
public void setPrimaryItem(ViewGroup container, int position, Object object) {
Fragment fragment = (Fragment)object;
if (fragment != mCurrentPrimaryItem) {
if (mCurrentPrimaryItem != null) {
mCurrentPrimaryItem.setMenuVisibility(false);
mCurrentPrimaryItem.setUserVisibleHint(false);
}
if (fragment != null) {
fragment.setMenuVisibility(true);
fragment.setUserVisibleHint(true);
}
mCurrentPrimaryItem = fragment;
}
}
在Viewpager嵌套Fragment的结构中,ViewPager.populate()这个函数非常重要。populate() 就是处理预加载,缓存的,并且跟adapter紧密关联.
那setUserVisibleHint是在生命周期的那个过程中被执行的呢?
从Log看:
setUserVisibleHint → onCreatedView, →onResume
并且其中的参数true表示fragment可见,false不可见,所以setUserVisibleHint就是要找的处理懒加载的时机.
2,懒加载怎么实现?
首先,这里定义一个抽象类,
在界面可见时才去加载界面更新需要的数据,界面不可见时,比如那些预加载的页面,只是加载默认界面(空白页面,或者loading页面),
所以有一个抽象函数getDefaultLayoutRes,,去定义默认界面长什么样,
默认界面怎么去设置,是由两一个抽象函数,setupDefaultView去完成,也就是onCreateView中那些findViewById获取控件,给控件设置内容的操作.
其次,不要按原来的机制,在onResume里面加载数据,而是依据懒加载机制,通过setUserVisibleHint里面的逻辑判断(具体的逻辑在代码中注释),通过可见,不可见标记来决定是否加载数据,这里也定义了需要子类去覆写的方法.
还有一种情况,Activity之间切换时,跟同一个Activity中的fragment之间切换是不同的,如何分发fragment的可见性.
在界面已经创建后,当其不可见后,也要通知Fragment界面不可见的相关操作(中断网路等).也就是建议子类去重写的两个函数:
onFragmentPause,onFragmentResume.
最后,考虑ViewPager的嵌套情况下(其中一个fragment又是一个Viewpager为框架的),上面的实现是否有坑.今日头条应该是这种两层嵌套的框架.
这种情况,在第一层viewpager切换时,那个有嵌套的fragment不可见时,也会加载其中(viewpager中的默认页面)的数据.这个bug如何修复?
修改策略,第一:在分发可见性状态时,是去判断父Fragment是否可见,只有父fragment可见时,才去执行数据加载,
第二个,要在父Fragment可见时,去为其中嵌套的fragment分发可见性状态.
这就是isParentInVisible, dispatchChildVisibleState做的事情.
有了上面的思路流程,来看代码就很容易理解了.
FragmentLazyLoading 是实现了懒加载的抽象类,FragmentLazyDemo 是示例。后面有输出的log,
public abstract class FragmentLazyLoading extends Fragment {
private static final String TAG = FragmentLazyLoading.class.getName();
//表示默认的界面,如loading,空白界面
protected View mDefaultView = null;
private boolean mIsViewCreated = false;
//是否是第一次创建,并可见
private boolean mIsFirstVisible = false;
//当前的可见标记
private boolean mCurrentVisibleState = false;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
if (mDefaultView == null) {
mDefaultView = inflater.inflate(getDefaultLayoutRes(), container, false);
}
//设置默认的界面,具体什么样子,需要子类去实现
setupDefaultView(mDefaultView);
mIsViewCreated = true;
Log.d(TAG,"onCreateView(),mDefaultView="+mDefaultView);
//这里分发tab中默认页的可见性,也即是初始的那个Fragment,
// 此时这个Fragment的getUserVisibleHint()为true,isHidden()为false,对于除默认页的其他页面,
// 只做了 mIsViewCreated =true。
if (!isHidden() && getUserVisibleHint()) {
dispatchUserVisibleHint(true);
}
return mDefaultView;
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
Log.d(TAG,"setUserVisibleHint(),isVisibleToUser="+isVisibleToUser);
//对于默认页的可见分发已经在onCreateView中做了,这个针对的是非默认页,
// 另外对一个界面做可见或者不可见的判断都有一个前提,就是这个页面已经创建了,
// 也即是经历过onCreateView,至少默认页面已经创建过
if (mIsViewCreated) {
//从不可见到可见,
if (isVisibleToUser && !mCurrentVisibleState) {
dispatchUserVisibleHint(true);
} else if (!isVisibleToUser && mCurrentVisibleState) {
//从可见到不可见
dispatchUserVisibleHint(false);
}
}
}
//分发可见性信息
private void dispatchUserVisibleHint(boolean visible) {
Log.d(TAG,"dispatchUserVisibleHint(),visible="+visible);
//针对嵌套viewpager的处理,父Fragment不可见不做处理
if (visible && isParentInVisible()) {
return;
}
if (mCurrentVisibleState == visible) {
//理论上,这里应该不会跑到
return;
}
//修改当前的可见标记
mCurrentVisibleState = visible;
if (visible) {
if (mIsFirstVisible) {
//Fragment第一次可见,会有很多事情做,单独拿出来
mIsFirstVisible = false;
onFragmentInit();
}
onFragmentResume();
//在父Fragment可见时,分发内嵌的Fragment的可见性状态
dispatchChildVisibleHint(true);
} else {
onFragmentPause();
//在父Fragment可见时,分发内嵌的Fragment的可见性状态
dispatchChildVisibleHint(true);
}
}
private void dispatchChildVisibleHint(boolean visible) {
FragmentManager fragmentManager = getChildFragmentManager();
List<Fragment> list = fragmentManager.getFragments();
if (null != list) {
for (Fragment fragment : list) {
if ((fragment instanceof FragmentLazyLoading) &&
!fragment.isHidden() &&
fragment.getUserVisibleHint()) {
((FragmentLazyLoading) fragment).dispatchUserVisibleHint(visible);
}
}
}
}
private boolean isParentInVisible() {
Fragment parentFragment = getParentFragment();
if (parentFragment instanceof FragmentLazyLoading) {
FragmentLazyLoading fragmentLazyLoading = (FragmentLazyLoading)parentFragment;
//因为要判断不可见,这里取反
return !fragmentLazyLoading.isSupportVisible();
}
return false;
}
private boolean isSupportVisible() {
return mCurrentVisibleState;
}
//为什么这里也需要重新分发呢?当用FragmentTransaction切换fragment,推荐的做法是hide,add,show,
// 在hide,show的时候,不会去执行fragment的生命周期,但是会执行
//onHiddenChanged,所以在这里也要做可见性分发
@Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
Log.d(TAG,"onHiddenChanged(),hidden="+hidden);
if (hidden) {
dispatchUserVisibleHint(false);
} else {
dispatchUserVisibleHint(true);
}
}
@Override
public void onResume() {
super.onResume();
//Log.d(TAG,"onResume()");
//第一次创建fragment总会执行onResume,但是此时,可能并不是可见的,如从fragment1到fragment2,
// 会去调用fragment3的onResume,这时fragment3并不可见,所以不需要处理分发。
if (!mIsFirstVisible) {
//当activity之间切换时,切换回来时,activity中的fragment都会执行其onResume,
// 但是这种情况下,不会执行setUserVisibleHint,因为它是有activity创建时,
// 通过绑定adapter来调用的,这一点从setUserVisibleHint的调用流程可以得出。
// 此时我们只需要对当前可见的fragment做分发。
if (!isHidden() && !mCurrentVisibleState && getUserVisibleHint()) {
dispatchUserVisibleHint(true);
}
}
}
@Override
public void onPause() {
super.onPause();
//Log.d(TAG,"onResume()");
//从可见到不可见就去销毁它
if (mCurrentVisibleState && !getUserVisibleHint()) {
dispatchUserVisibleHint(false);
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
mIsViewCreated = false;
mIsFirstVisible = false;
}
//建议子类,应该去覆写这个方法,通知可见状态
public void onFragmentPause() {
}
//建议子类,应该去覆写这个方法,通知不可见状态
public void onFragmentResume() {
}
//第一次创建fragment,
protected abstract void onFragmentInit();
//设置默认界面显示内容
protected abstract void setupDefaultView(View mDefaultView);
//默认界面的布局
protected abstract int getDefaultLayoutRes();
}
--
public class FragmentLazyDemo extends FragmentLazyLoading {
private static final String TAG = FragmentLazyDemo.class.getName();
private static final String INTENT_INDEX = "intent.index";
private static final int LOAD_DATA_DONE = 0;
private ImageView mImageView;
private TextView mTextView;
private int mNavigationIndex;
private CountDownTimer mCountDownTimer;
public static FragmentLazyDemo newInstance(int navigationIndex) {
Bundle bundle = new Bundle();
bundle.putInt(INTENT_INDEX , navigationIndex);
FragmentLazyDemo fragmentDemo = new FragmentLazyDemo();
fragmentDemo.setArguments(bundle);
return fragmentDemo;
}
@Override
protected int getDefaultLayoutRes() {
return R.layout.fragment_layout;
}
@Override
protected void setupDefaultView(View view) {
mImageView = view.findViewById(R.id.iv_content);
mTextView = view.findViewById(R.id.tv_loading);
mNavigationIndex = getArguments().getInt(INTENT_INDEX);
Log.d(TAG, "setupDefaultView(),fragment "+mNavigationIndex + "设置默认界面 " );
}
@Override
protected void onFragmentInit() {
Log.d(TAG, "onFragmentInit(),fragment "+ mNavigationIndex + "第一次创建" );
}
@Override
public void onFragmentResume() {
super.onFragmentResume();
loadData();
Log.d(TAG, " onFragmentResume(),fragment "+ mNavigationIndex + "更新界面" );
}
@Override
public void onFragmentPause() {
super.onFragmentPause();
mHandler.removeMessages(10);
mCountDownTimer.cancel();
Log.d(TAG, "onFragmentPause(), fragment " +mNavigationIndex+ "暂停操作" );
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
mNavigationIndex = getArguments().getInt(INTENT_INDEX);
Log.d(TAG,"setUserVisibleHint()--,fragment " + mNavigationIndex +
",isVisibleToUser="+isVisibleToUser);
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
mNavigationIndex = getArguments().getInt(INTENT_INDEX);
//Log.d(TAG,"onAttach()--,fragment " + mNavigationIndex);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//Log.d(TAG,"onCreate()--,fragment " + mNavigationIndex);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
//Log.d(TAG, "onCreateView()--, fragment " + mNavigationIndex);
return super.onCreateView(inflater,container,savedInstanceState);
}
private void loadData() {
//模拟耗时
mCountDownTimer = new CountDownTimer(1000, 100) {
@Override
public void onTick(long millisUntilFinished) {
}
@Override
public void onFinish() {
mHandler.sendEmptyMessage(LOAD_DATA_DONE);
}
};
mCountDownTimer.start();
}
@Override
public void onResume() {
super.onResume();
Log.d(TAG, "onResume()--, fragment " + mNavigationIndex);
}
@Override
public void onPause() {
super.onPause();
Log.d(TAG, "onPause()--, fragment " + mNavigationIndex);
}
@Override
public void onStop() {
super.onStop();
//Log.d(TAG, "onStop()--, fragment " + mNavigationIndex);
}
@Override
public void onDestroyView() {
super.onDestroyView();
if (null != mCountDownTimer) {
mCountDownTimer.cancel();
}
Log.d(TAG, "onDestroyView()--, fragment " + mNavigationIndex);
}
@Override
public void onDestroy() {
super.onDestroy();
//Log.d(TAG, "onDestroy()--, fragment " + mNavigationIndex);
}
@Override
public void onDetach() {
super.onDetach();
//Log.d(TAG, "onDetach()--, fragment " + mNavigationIndex);
}
private Handler mHandler = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {
if (msg.what == LOAD_DATA_DONE) {
mTextView.setVisibility(View.GONE);
int resId = R.drawable.f1;
switch (mNavigationIndex) {
case 1:
resId = R.drawable.f1;
break;
case 2:
resId = R.drawable.f2;
break;
case 3:
resId = R.drawable.f3;
break;
case 4:
resId = R.drawable.f4;
break;
case 5:
resId = R.drawable.f5;
break;
}
mImageView.setImageResource(resId);
mImageView.setScaleType(ImageView.ScaleType.FIT_XY);
mImageView.setVisibility(View.VISIBLE);
//Log.d(TAG,"handleMessage()--,fragment " + mNavigationIndex);
try {
Thread.sleep(200);
}catch (Exception e){
e.printStackTrace();
}
}
}
};
}
输出log:
2020-02-13 11:07:24.248 21198-21198myviewpager.lazyLoad.FragmentLazyDemo: setUserVisibleHint()--,fragment 1,isVisibleToUser=false
2020-02-13 11:07:24.248 21198-21198.myviewpager.lazyLoad.FragmentLazyLoading: setUserVisibleHint(),isVisibleToUser=false
2020-02-13 11:07:24.248 21198-21198.myviewpager.lazyLoad.FragmentLazyDemo: setUserVisibleHint()--,fragment 2,isVisibleToUser=false
2020-02-13 11:07:24.248 21198-21198.myviewpager.lazyLoad.FragmentLazyLoading: setUserVisibleHint(),isVisibleToUser=true
2020-02-13 11:07:24.248 21198-21198myviewpager.lazyLoad.FragmentLazyDemo: setUserVisibleHint()--,fragment 1,isVisibleToUser=true
2020-02-13 11:07:24.256 21198-21198myviewpager.lazyLoad.FragmentLazyDemo: setupDefaultView(),fragment 1设置默认界面
2020-02-13 11:07:24.256 21198-21198myviewpager.lazyLoad.FragmentLazyLoading: onCreateView(),mDefaultView=android.widget.FrameLayout{c2bd86d V.E...... ......I. 0,0-0,0}
2020-02-13 11:07:24.256 21198-21198myviewpager.lazyLoad.FragmentLazyLoading: dispatchUserVisibleHint(),visible=true
2020-02-13 11:07:24.257 21198-21198.myviewpager.lazyLoad.FragmentLazyDemo: onFragmentResume(),fragment 1更新界面
2020-02-13 11:07:24.259 21198-21198myviewpager.lazyLoad.FragmentLazyDemo: onResume()--, fragment 1
2020-02-13 11:07:24.260 21198-21198myviewpager.lazyLoad.FragmentLazyDemo: setupDefaultView(),fragment 2设置默认界面
2020-02-13 11:07:24.261 21198-21198myviewpager.lazyLoad.FragmentLazyLoading: onCreateView(),mDefaultView=android.widget.FrameLayout{1036d33 V.E...... ......I. 0,0-0,0}
2020-02-13 11:07:24.261 21198-21198myviewpager.lazyLoad.FragmentLazyDemo: onResume()--, fragment 2
从Log,界面可见时才去执行更新界面内容的操作,
如果界面不可见,仅加载默认界面(空白页,或者loading页面)
2020-02-13 14:39:57.624 23633-23633// D/com.test.myviewpager.lazyLoad.FragmentLazyLoading: setUserVisibleHint(),isVisibleToUser=false
2020-02-13 14:39:57.624 23633-23633// D//.lazyLoad.FragmentLazyDemo: setUserVisibleHint()--,fragment 3,isVisibleToUser=false
2020-02-13 14:39:57.624 23633-23633// D//.lazyLoad.FragmentLazyLoading: setUserVisibleHint(),isVisibleToUser=false
2020-02-13 14:39:57.624 23633-23633// D//.lazyLoad.FragmentLazyLoading: dispatchUserVisibleHint(),visible=false
2020-02-13 14:39:57.624 23633-23633// D//.lazyLoad.FragmentLazyDemo: onFragmentPause(), fragment 1暂停操作
2020-02-13 14:39:57.624 23633-23633// D//.lazyLoad.FragmentLazyDemo: setUserVisibleHint()--,fragment 1,isVisibleToUser=false
2020-02-13 14:39:57.624 23633-23633// D//.lazyLoad.FragmentLazyLoading: setUserVisibleHint(),isVisibleToUser=true
2020-02-13 14:39:57.624 23633-23633// D//.lazyLoad.FragmentLazyLoading: dispatchUserVisibleHint(),visible=true
2020-02-13 14:39:57.624 23633-23633// D//.lazyLoad.FragmentLazyDemo: onFragmentResume(),fragment 2更新界面
2020-02-13 14:39:57.624 23633-23633// D//.lazyLoad.FragmentLazyDemo: setUserVisibleHint()--,fragment 2,isVisibleToUser=true
2020-02-13 14:39:57.627 23633-23633// D//.lazyLoad.FragmentLazyDemo: setupDefaultView(),fragment 3设置默认界面
2020-02-13 14:39:57.627 23633-23633// D//.lazyLoad.FragmentLazyLoading: onCreateView(),mDefaultView=android.widget.FrameLayout{964a825 V.E...... ......I. 0,0-0,0}
2020-02-13 14:39:57.628 23633-23633// D//.lazyLoad.FragmentLazyDemo: onResume()--, fragment 3
从Fragment1滑到Fragment2,可以看到Fragment 1 执行暂停,Fragment 2 更新界面, Fragment 3 仅设置默认界面。