2018
写在前面:这篇文章主要分析导航栏,也就是ViewPager+Fragment+FragmentPagerAdapter的懒加载模式,重点还是Fragment生命周期的应用。将这两块放在一起讲是因为两者不可分割,很多教程没有结合两者,大家也是糊里糊涂地用着代码,这次就讲个清楚。
如果对如何使用导航栏还不太了解,请大家移步 底部导航栏标签切换的实现 这篇文章,里面有博主的一份Demo与一些坑点介绍。
1、QQ看点
自从手机“QQ看点”合并到QQ底部导航栏之后,功能可以说是又丰富了很多,这里还是要赞一下推荐系统,真的是让人刷得停不下来。不过言归正传,我们可以很容易发现在点击切换到“看点”的选项卡的时候,页面会有一次自动刷新,说明数据进行了一次拉取。这种设计的优点很明显,我不需要用户手动刷新,也不会发生我在其他选项卡里聊天、发说说的时候突然来拉取一波数据的情况,不然这后台得默默帮我消费多少流量啊。
接下来就通过分析Fragment 的生命周期,让大家清楚了解我要调用哪个方法才是正确的“懒加载”。注意是正确的写法,“懒加载”的方式网上一写一大把,不过很多其实都是比较含糊的,不是代码问题,而是业务场景没有交代清楚——从哪里切换回来Fragment会调用什么方法?
2、Fragment生命周期
- 第一个知识点:大家都知道 Fragment 是绑定 Activity 的,不过,很多人会忽视了它们的生命周期也是会一定程度上同步的。下面表格的场景是在 Activity 内部实例化 Fragment,Fragment 与 Activity 生命周期之间的关系。
Activity | Fragment |
---|---|
onCreate() | onAttach(), onCreateView(), onCreateView, onActivityCreate() |
onStart() | onStart() |
onResume() | onResume() |
onPause() | onPause() |
onStop() | onStop() |
onDestroy() | onDestroyView(), onDestroy(), onDetach() |
大多数时候我们将实例化 Fragment 放在 Activity.onCreate()
中,可能是new,也可能是单实例,不难理解 Fragment 在Activity.onCreate()
之后才开始一系列生命周期方法的调用。而其他方法执行顺序会是下面这样:
Activity.onStart()-->Fragment.onStart()
Activity.onResume()-->Fragment.onResume()
Fragment.onPause()-->Activity.onPause()
Fragment.onStop()-->Activity.onStop()
Fragment.onDestroyView(),onDestroy(),onDetach()-->Activity.onDestroy()
Fragment 依赖于 Activity,故而,Fragment创建于Acivity之后,销毁于Acrivity之前。
- 第二个知识点:结合 ViewPager 后,页面切换会有哪些生命周期方法的调用。看过上一篇博文的朋友应该都比较清楚了,这里就再不厌其烦地给没看过的朋友讲解一下。
单独使用Fragment没什么好说的,调用的方法和上表一样,会和 Activity 同步。而结合 ViewPager 取决于选择哪一种适配器,现在大多数使用 FragmentPagerAdapter或者 FragmentPagerStateAdapter,两者的区别无非就是到底会不会回收较远页面的内存。
前者只回收View,除非内存不足,否则不会销毁加载好的Fragment;后者为达到节省内存的目的,对于不在当前页面左右两边的其他Page,将会销毁其Fragment,只保留最多三个页面在内存中,当然,如果当前页面是第一页或者最后一页,那就只有两页在内存中了。
- 针对 FragmentPagerAdapter ,因为不会回收Fragment,[1]切换到已经加载好的Fragment 只需要设置为可见,即调用
setUserVisibleHint(true)
,而原来的页面设置为不可见setUserVisibleHint(false)
,[2]而切换到未加载的 Fragment ,如果该Fragment 之前加载过,仅仅是被回收了View,就会执行onCreateView()-->onActivityCreate()-->onStart()-->onResume()
,反之,之前没有加载历史的就从onAttach()
开始。
详细的测试日志如下,亲自测试了一下,在上一篇博文的基础上添加了打印日志,Fragment-->
代表第一个Fragment (第一页)的生命周期,Activity-->
代表与之关联的活动的生命周期:
//[1]启动活动,注意到第一个Fragment一开始就是可见的,而且先调用两次相反的setUserVisibleHint()来初始化
I/MyTest: Activity-->onCreate()
I/MyTest: Activity-->onStart()
Activity-->onResume()
I/MyTest: Fragment-->getUserVisibleHint:true //可见
I/MyTest: Fragment-->setUserVisibleHint(false) //设置为不可见
Fragment-->getUserVisibleHint:false
Fragment-->setUserVisibleHint(true) //设置为可见
I/MyTest: Fragment-->onAttach()
Fragment-->onCreate()
I/MyTest: Fragment-->onCreateView()
I/MyTest: Fragment-->onActivityCreated()
Fragment-->onStart()
Fragment-->onResume()
//[2]点击第三个页面,只设置第一页不可见和回收View,当然第三个页面也执行了和第一页同样的初始化
I/MyTest: Fragment-->getUserVisibleHint:true
Fragment-->setUserVisibleHint(false) //设置为不可见
I/MyTest: Fragment-->onPause()
I/MyTest: Fragment-->onStop()
Fragment-->onDestroyView()
//[3]返回第一个页面,先调用setUserVisibleHint(true),再从onCreateView()开始加载View
I/MyTest: Fragment-->getUserVisibleHint:false
Fragment-->setUserVisibleHint(false) //设置为不可见
Fragment-->getUserVisibleHint:false
Fragment-->setUserVisibleHint(true) //设置为可见
Fragment-->onCreateView()
Fragment-->onActivityCreated()
Fragment-->onStart()
Fragment-->onResume()
//[4]销毁活动,果然和Activity同步了,当然,同样预加载的所有页面都是同步的
I/MyTest: Fragment-->onPause()
Activity-->onPause()
I/MyTest: Fragment-->onStop()
I/MyTest: Activity-->onStop()
I/MyTest: Fragment-->onDestroyView()
Fragment-->onDestroy()
Fragment-->onDetach()
Activity-->onDestroy()
小知识点:
(1)Fragment初始化的时候会执行两次setUserVisibleHint()
,一次false,一次true。
(2)切换到不是 当前页面左右两边的页面 时,当前页面会被回收View,但不被销毁,原因是Adapter默认只完整加载左右两边页面。
(3)切换到 当前页面左右两边的页面 时,只会调用setUserVisibleHint(true)
。
(3)Fragment 的生命周期与Activity息息相关。经过另外的测试,如果Activity变得不可见(比如开启新活动),那么所有加载好的Fragment会和Activity一起执行onPause()
,onStop()
,返回时又会一起执行onStart()
,onResume()
,这就是生命周期的同步。
(4)初始化的时候setUserVisibleHint()
比Fragment任何生命周期方法要早调用。而从较远页面切换回来setUserVisibleHint()
也至少比onCreateView()
先执行,如页面3到页面1。
(5)页面的切换都会使用到 setUserVisibleHint()
。
3、懒加载在不同业务场景下的实现
有了上面的知识储备,灵活地使用“懒加载”也不难了,根据业务需要,我们可以设置不同的加载方式。
基本方法:使用
setUserVisibleHint()
,在进入一个页面之前我们肯定会需要调用这个方法,将数据拉取放在这里这不正合我们意吗?
(1)场景1:只需要拉取一次数据,但是很可能整个活动期间都不会切换到这个页面,所以希望省点流量,只有切换到才加载,而且只有一次。写法如下:
private boolean hasLoad;
@Override
public void setUserVisibleHint(boolean isVisible){
//设置为可见并且没有加载过数据的时候进行网络请求
if(isVisible && (!hasLoad)){
/*
* 网路请求数据
*/
hasLoad = true;
}
super.setUserVisibleHint(isVisible);
}
(2)场景2:每次进入页面都要拉取数据,但只要加载数据成员不需要更新UI,下面这种写法就可以了。
@Override
public void setUserVisibleHint(boolean isVisible){
if(isVisible){
/*
* 网路请求数据
*/
}
super.setUserVisibleHint(isVisible);
}
(3)场景3:每次进入都需要拉取数据,数据加载后要更新UI。
为什么和第一种区分,留意小知识点(4)的两种情况,此时Fragment还没有加载View实例,而更新UI必须要确保已经获取View实例。那不妨使用一个boolean变量来标识获取View的状态,确保View获取了就加载,反之不加载。很有道理,不过还有缺陷,因为等View加载完,我们已经不会调用setUserVisibleHint()
了,所以还要在View获取后完成一次补充加载。
下面这种写法就能确保无论是初始化、还是从较远页面切换过来或者从左右页面切换过来,都能马上拉取数据。大家可以直接用这种格式哦。
private boolean hasView;
@Override
public void setUserVisibleHint(boolean isVisible){
if(isVisible && hasView){
/*
* 网路请求数据 + 更新UI
*/
}
super.setUserVisibleHint(isVisible);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_layout_1, container,false); //动态加载布局
/*
* 使用 view.findViewById(long resourceId)来获取View实例
*/
hasView = true; //View加载好了
if(getUserVisibleHint()){
/*
* 数据请求 + UI更新
*/
}
return view;
}
@Override
public void onDestroyView(){
hasView=false; // View被回收了
super.onDestroyView();
}
小技巧:留意上面onCreateView()
中的 if
代码块,这就是我们说的,如果是没有View的情况下不能直接使用 setUserVisibleHint()
加载,需要之后采取一个补充加载。
同学甲这时也许会问:既然获取View了为什么不直接加载,为什么还要判断可见性?哈哈,这是被绕晕了。请不要忘记哦,我们是不打开页面就不会加载数据的,如果只是单纯的预加载,
onCreateView()
也是会执行的,当然要防止这种情况。同学乙又问:既然只是补充加载,那么我只要在View实例获取之后加载就行了吧,比如
onStart()
、onResume()
,反正只要判断可见性就不会误加载?是的,确实是这样,这几个方法在 Fragment 中一起被调用的场景很多,比如开启了一个新的Activity后返回。但要注意,由于生命周期绑定Activity,如果你是开启新活动后返回,所有预加载完成的Fragment 是会和Activity一起执行onStart()
、onResume()
的,你要考虑是不是要这样的效果。同学丙也有问题:既然第三种场景中数据已经实现了“懒加载”,那么
findViewById()
之类的获取控件实例能不能也懒一下,将些代码也并入if
中,这样是不是也能节省一些获取实例的开销?听起来好像是这么回事,点进去才获取实例。但是你要清楚这种情况更适用于第一种场景的UI更新。因为,你需要在setUserVisibleHint()
中也添加这些获取实例的代码,如果是频繁在两个页面之间切换,那就不是节省了。当然可以用一些手段来修复一下,可以再添加一个标志位,逻辑也不难,毕竟我们已经使用两个标志了,就留给各位慢慢研究吧。
4、强调ViewPager中四种不同的Fragment状态:
(1)当前页面:Fragment 生命周期执行到onResume()
,可见性getUserVisibleHint()
为true
。
(2)预加载页面:位于当前页面左右两边,Fragment 生命周期执行到onResume()
,
可见性getUserVisibleHint()
为false
。
(3)曾经加载过但是被回收了View的页面:肯定不在当前页面左右,Fragment 生命周期执行到onCreate()
,getUserVisibleHint()
为false
。
(4)从来没有加载过的页面:也肯定不在当前页面左右,所有方法都还没调用,getUserVisibleHint()
为false
。
到这里,关于数据懒加载我们也了解得差不多了,如果还有其他场景的需要,不妨评论一下,我们一起交流。也欢迎大家来 我的主页 查看其他相关博文。如有不正之处欢迎指正。