码不停蹄(四):Android分析Fragment生命周期与实现导航栏的“懒加载”——模仿“QQ看点”

版权声明:本文所有内容均为博主原创手打,转载请注明出处,如有雷同,不甚荣幸。 https://blog.csdn.net/Xu_JL1997/article/details/82504093

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


到这里,关于数据懒加载我们也了解得差不多了,如果还有其他场景的需要,不妨评论一下,我们一起交流。也欢迎大家来 我的主页 查看其他相关博文。如有不正之处欢迎指正。

猜你喜欢

转载自blog.csdn.net/Xu_JL1997/article/details/82504093