从FragmentPagerAdapter看Fragment 生命周期
之前写过一个关于Fragment使用的帖子,但是并没有对生命周期做太详细的了解。因为那个时候主要是首页上的四个页签,然后用FrameLayout进行管理,使用那个显示哪个就可以了,然后在onCreateView里面初始化并返回我们要使用的View即可。
但是最近看到了一种写法就是一个Activity中放置多个数据显示格式相同,按内容划分的Fragment,而且确实按照正常无优化的方法,随着浏览时间的加长,数据量的曾加会先出现卡顿,然后内存溢出的问题。
那么就此场景举一个最常见的例子我的订单。(为了让viewpager有明显的过场,我每个Fragment设置的从红色开始的彩虹7色)
大家可以分析一下,订单的状态是不一样的,但是一般数据的显示格式是一样的。那么这个时候我们的设计思想就是首先订单是一个Activity,然后往里面放一个Viewpager,在写一个适配器,然后,将单个订单页封装成一个Fragment根据构造,然后请求不同的数据,那么就实现了,可是经过实践发现,没有想的那么简单。那么记录整个过程是非常有学习价值的所以我打算一步一步的拆解实现的过程,也是一个学习总结再升华的过程。
一、使用Fragment的小窍门,使用appV4下的Fragment而不是使用App包下的Fragment。为什么还是推荐V4下的Fragment?
1.当我们的使用第三方的时候会发现,第三方为了让自己的控件最大化的兼容(通常会将一套功能封装成一个Fragment),会选择V4下的Fragment,而我们如果使用app下的Fragment那么他FragmentManager是无法管理V4下的Fragment的,只有V4下的FragmentActivity所提供的supportFragmentManager才能管理V4下的Fragment所以为了让程序更好的兼容。
2.当我们使用Viewpager来管理Fragment的时候我们使用的FragmentPagerAdapter构造器中其FragmentManager是来自于AppV4包下面的,所以为了让我们自己的Fragment有更好的通用和适配性,这里推荐大家使用V4的Fragment为以后的扩充做好准备。
二、小窍门中我们说了使用Viewpager和Fragment相结合需要用到适配器。那么我们先来看下适配器,及带来的问题。
问题1:如果Fragment有网络加载,那么我们会发现除了当前的Fragment的网络启动了之外,别的Fragment的网络加载也启动了!
2:显示上没问题,左右滑动也都没事,但是越用越卡
3:如何利用Fragment的生命周期让我们的页面顺滑起来。
那么以上是我在这个开发过程中遇到的主要问题。
问题1:如果Fragment有网络加载,那么我们会发现除了当前的Fragment的网络启动了之外,别的Fragment的网络加载也启动了!
这里要说的是FragmentPagerAdapter的加载机制,为了让客户有更好的体验,那么当我们用ViewPager显示一个Fragment的时候,FragmentPagerAdapter会自动加载其两侧的Fragment,进而让客户滑动的时候感觉没有明显的卡顿和数据加载,但是如果我们写的网络加载里面有Toast的话,那么就会发生一些乌龙时间,好比第一页是有数据的,这个时候数据已经正常显示了,但是我在无数据的时候给了一个无数据提示,而第二页恰好没数据,那么乌龙就发生了,在有数据的页接到了无数据提示。而且更主要的是,如果我们的订单约定是是显示10条,那么同时开启两个数据加载链接必然产生两个请求数据返回的时间加长!这样对应当前想使用的页体验又是不好的。在一个我们缓存两页,如果图片过多,又没有写好数据回收的工作,那么刚开始就是卡顿,再然后就是内存溢出。
问题2:就是左右加载的内存没有被释放导致的,这里建议使用Fresco去加载图片,因为Fresco的内存处理机制实在是太棒了!嗯这个问题点也主要是想说这个,当我使用Fresco的缓存策略的时候,只要图片不在当前页面的显示那么图片的内存就被回收了。这个问题是之前使用Glide导致的,因为内存回收的不及时,最后导致滑动了几次之后就崩溃了。
问题3:这里就是要说一下当我们滑动的时候,我们需要注意一下在FragmentPagerAdapter下的Fragment的生命周期。
当我们从Activity进入一个Fragment的时候,其流程是
显示出来的页:onAttach->onCreate->onCreateView->onActivityCreated ->onStart ->onResume
之后两边的页:onAttach->onCreate->onCreateView->onActivityCreated ->onStart ->onResume
打印Log的时候我们可以看见同样的生命周期会输出三遍这就是说当前的Fragment和左右的Fragment都被加载了。所以当我们将网络加载的代码放到onCreateView里面的时候,当我们开启一个Fragment那么其实同时开启了3个网络加载线程!所以这是我们不希望的。这就可能导致之前说的乌龙事件。这里给出的解决策略是在onsetUserVisibleHint()方法中进行数据加载,当我们的Fragment显示的时候,启动数据加载单元,那么我们的Fragment其实只是初始化了一些固定View并不会占用太多的内存。
那么我们继续往下走,当我们滑动Fragment的时候会看见那些生命周期那?
被隐藏的页面:onPause->onStop->onDestroyView
新建的页面:onAttach->onCreate->onCreateView->onActivityCreated ->onStart ->onResume
这里有一个重点,就是Fragment的生命周期到onDestroyView就结束了,而并是onDestory,这是很重要的,这就说明只有我们和Fragment.RootView相关的View被销毁了,而其他的数据还在。就那这个列表页来说,我需要写下来刷新和上拉加载,那么我就需要一个全局的Adapter那么在Fragment#onDestroyView的时候我的数据就还在,那么我就可以结合之前说的onsetUserVisibleHint()中根据adapter是否被初始化来决定是否再次进行网络加载!进而起到减少网络加载,和在使用时加载的需求。
这里继续为大家补充一些关于setUserVisibleHint的知识:
setUserVisibleHint()在Fragment创建时会先被调用一次,传入isVisibleToUser = false
如果当前Fragment可见,那么setUserVisibleHint()会再次被调用一次,传入isVisibleToUser = true
如果Fragment从可见->不可见,那么setUserVisibleHint()也会被调用,传入isVisibleToUser = false
总结:setUserVisibleHint()除了Fragment的可见状态发生变化时会被回调外,在new Fragment()时也会被回调
如果我们需要在 Fragment 可见与不可见时干点事,用这个的话就会有多余的回调了,那么就需要重新封装一下
这里贴出来封装好的Fragment
public abstract class LazyFragment extends Fragment {
private static final String TAG = LazyFragment.class.getSimpleName();
private boolean isFragmentVisible;
private boolean isReuseView;
private boolean isFirstVisible;
private View rootView;
//setUserVisibleHint()在Fragment创建时会先被调用一次,传入isVisibleToUser = false
//如果当前Fragment可见,那么setUserVisibleHint()会再次被调用一次,传入isVisibleToUser = true
//如果Fragment从可见->不可见,那么setUserVisibleHint()也会被调用,传入isVisibleToUser = false
//总结:setUserVisibleHint()除了Fragment的可见状态发生变化时会被回调外,在new Fragment()时也会被回调
//如果我们需要在 Fragment 可见与不可见时干点事,用这个的话就会有多余的回调了,那么就需要重新封装一个
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
//setUserVisibleHint()有可能在fragment的生命周期外被调用
if (rootView == null) {
return;
}
if (isFirstVisible && isVisibleToUser) {
onFragmentFirstVisible();
isFirstVisible = false;
}
if (isVisibleToUser) {
onFragmentVisibleChange(true);
isFragmentVisible = true;
return;
}
if (isFragmentVisible) {
isFragmentVisible = false;
onFragmentVisibleChange(false);
}
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initVariable();
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
//如果setUserVisibleHint()在rootView创建前调用时,那么
//就等到rootView创建完后才回调onFragmentVisibleChange(true)
//保证onFragmentVisibleChange()的回调发生在rootView创建完成之后,以便支持ui操作
if (rootView == null) {
rootView = view;
if (getUserVisibleHint()) {
if (isFirstVisible) {
onFragmentFirstVisible();
isFirstVisible = false;
}
onFragmentVisibleChange(true);
isFragmentVisible = true;
}
}
super.onViewCreated(isReuseView ? rootView : view, savedInstanceState);
}
@Override
public void onDestroyView() {
super.onDestroyView();
}
@Override
public void onDestroy() {
super.onDestroy();
initVariable();
}
private void initVariable() {
isFirstVisible = true;
isFragmentVisible = false;
rootView = null;
isReuseView = true;
}
/**
* 设置是否使用 view 的复用,默认开启
* view 的复用是指,ViewPager 在销毁和重建 Fragment 时会不断调用 onCreateView() -> onDestroyView()
* 之间的生命函数,这样可能会出现重复创建 view 的情况,导致界面上显示多个相同的 Fragment
* view 的复用其实就是指保存第一次创建的 view,后面再 onCreateView() 时直接返回第一次创建的 view
*
* @param isReuse
*/
protected void reuseView(boolean isReuse) {
isReuseView = isReuse;
}
/**
* 去除setUserVisibleHint()多余的回调场景,保证只有当fragment可见状态发生变化时才回调
* 回调时机在view创建完后,所以支持ui操作,解决在setUserVisibleHint()里进行ui操作有可能报null异常的问题
*
* 可在该回调方法里进行一些ui显示与隐藏,比如加载框的显示和隐藏
*
* @param isVisible true 不可见 -> 可见
* false 可见 -> 不可见
*/
protected void onFragmentVisibleChange(boolean isVisible) {
}
/**
* 在fragment首次可见时回调,可在这里进行加载数据,保证只在第一次打开Fragment时才会加载数据,
* 这样就可以防止每次进入都重复加载数据
* 该方法会在 onFragmentVisibleChange() 之前调用,所以第一次打开时,可以用一个全局变量表示数据下载状态,
* 然后在该方法内将状态设置为下载状态,接着去执行下载的任务
* 最后在 onFragmentVisibleChange() 里根据数据下载状态来控制下载进度ui控件的显示与隐藏
*/
protected void onFragmentFirstVisible() {
}
protected boolean isFragmentVisible() {
return isFragmentVisible;
}
}
FragmentStatePagerAdapter和FragmentPagerAdapter的比较
1.FragmentPagerAdapter适用于Fragment比较少的情况,因为我们会把每一个Fragment保存在内存中,不用每次切换的时候,去保存现场,切换回来在重新创建,所以用户体验比较好。没有onDestored()!
2.而对于我们的Fragment比较多的情况,我们需要切换的时候销毁以前的Fragment以释放内存,就可以使用FragmentStatePagerAdapter。onDestored()!