天天看点

Fragment 生命周期的坑- 记一次毫无防备的无知Fragment 生命周期的坑- 记一次毫无防备的无知

Fragment 生命周期的坑- 记一次毫无防备的无知

项目中最近有个需求是这样的,主页面包含 3 个 Fragment,对应底部三个 Tab,如下图显示:

Fragment 生命周期的坑- 记一次毫无防备的无知Fragment 生命周期的坑- 记一次毫无防备的无知
Fragment 生命周期的坑- 记一次毫无防备的无知Fragment 生命周期的坑- 记一次毫无防备的无知
Fragment 生命周期的坑- 记一次毫无防备的无知Fragment 生命周期的坑- 记一次毫无防备的无知

相信大家对这种需求并不陌生,实现方式是自定义 Toolbar 做页面的 title,当然本文的中点不在于介绍如何自定义该标题栏。而是在于 Fragment 如何控制标题栏的改变。

问题出现在我是用的是 Fragment 的 add 方法将Fragment添加到Activity中,然后使用 hide 和 show 方法对 Fragment 进行显示和隐藏。 直到今天自己在把弄 App 的时候发现,在通过底部 tab 切换 Fragment 的时候,左侧的图标(圆形用户头像和加号会恰如其份的显示和替换),但是当我替换从第一个,或第二个 tab 跳转到二级页面的以后再回来,左侧图标就变成了加号。 这让我很懊恼,我尝试这从对应的 Fragment 的 onResume 和承载这三个 Fragment 的 Activity 的 onResume 方法中去强制改变这个图标显示,但发现这种方法并不是那么可行。

本着「先修复bug,在找到原因的做法」,梳理了整个页面初始化的流程。然后在生命周期内部都打印了log,发现一个「奇怪」的现象,我发现随着 MainActivity 的 onResume 执行,所有被添加到这个 Activity 的 Fragment 都执行各自的 onResume方法都被执行:

MainActivity 的 onResume
HomeFragment 的 onResume
SubsFragment 的 onResume
OptionalFragment 的 onResume
           

于是我知道我的错误在哪里了,我可能在第三个 Framgment 的 onResume 方法中替换了左侧按钮的图片。导致从其他页面回来 onResume 按顺序执行,最后执行 optionalFragment 所以看到的按钮变成了加号。

经过以上的分析bug是解决了一半了,但是疑问来了:

  1. 面试被问烂的 Fragment 生命周期你真的了解么 ?
  2. Fragment 的两种管理方式对应的生命周期有什么不同 ?
  3. 应该在哪个生命周期做用户可见的必要操作?

Fragment 的生命周期

老生常谈,相信大家都能倒背如流了,来一起背一下:「onAttach」,「onCreate」,「onCreateView」,「onActivityCreated」,「onStart」,「onResume」,「onStop」,「onDestroyView」,「onDestroy」,「onDetach」。背不下来的自己默写一百遍。哈哈~

其实重要的不是会背这些生命周期方法,但是而是明白对应的对应的生命周期该做什么,还有与宿主 Activity 生命周期的关系:

  • onAttach()在片段已与 Activity 关联时调用(Activity 传递到此方法内)。
  • onCreateView()调用它可创建与片段关联的视图层次结构。
  • onActivityCreated() 所在 Activity 的 onCreate() 方法已返回时调用。
  • onDestroyView()在移除与片段关联的视图层次结构时调用。
  • onDetach()在取消片段与 Activity 的关联时调用(remove 方法调用)。
Fragment 生命周期的坑- 记一次毫无防备的无知Fragment 生命周期的坑- 记一次毫无防备的无知

上图是 Google 官方教程中的图片,很好的说明了 Fragment 是如何在宿主 Activity 中进行自己的初始化的。 但是对于实际开发这并不足以让我们绕过上述我遇到的问题。

「与 Activity 生命周期协调一致」

相信大家在初学的时候都听过这么一句话,「Fragment 的生命周期与 Activity 生命周期协调一致」,以前天真的以为这里所说的协调一致是指,上边那副图中生命周期的对应关系,其实没错,当宿主 Activity 只管理一个 Fragment 的时候,对应关系就是这样。但是当时没有考虑到这样这样的项目需求所遇见的问题。那么我们来根据官方文档,回头再来看看这句话的意思是:

「片段所在的 Activity 的生命周期会直接影响片段的生命周期,其表现为,Activity 的每次生命周期回调都会引发每个片段的类似回调。 例如,当 Activity 收到 onPause() 时,Activity 中的每个片段也会收到 onPause()。」

注意上述标注部分,每次生命周期的回调会引起,其内部每个片段对应生命周期方法的回调。这就不难解释,之前说的打印结果。

Fragment 两种管理方式

大家都知道,我们在 Activity 中可以通过 FragmentManager 和 FragmentTransaction 来管理 activity 的内部的碎片,其管理方式有两种常用方法:

  1. 每次添加和替换都通过 replace 方法,替换碎片区域内容。
  2. 通过 add show hide 通过显示隐藏来管理Fragment。

以上两种管理方式虽然在用户界面上看上去并无太大差别,但是在生命周期上却有着明显的区别。以下所说的管理方法仅当 Fragment 个数为两个以上时有效:

第一种方案:add show hide 管理方法

首先看下当我们调用 add 方法将两个 Fragment 添加到 Activity 的时候 Fragment 的生命周期,

E/Fragment1: onAttach
E/Fragment1: onCreate
E/Fragment1: onCreateView
E/Fragment1: onActivityCreated
E/Fragment1: onStart
E/Fragment1: onResume

E/Fragment2: onAttach
E/Fragment2: onCreate
E/Fragment2: onCreateView
E/Fragment2: onActivityCreated
E/Fragment2: onStart
E/Fragment2: onResume

           

可以看到截止到 Activity 生命周期走到 onResume ,即页面可见状态,通常我们会以为首先可见的 Fragment 会走其生命周期,但是通过 log 日志看出,如果我们调用了 add 方法,所 add 进去的Fragment都会跟随其宿主 Activity 的生命周期,其实实际开发中我们是可以控制一个 Fragment 先走的,第二个 Fragment 再走的,就是在我们点击第二个tab的时候在调用第二个 Fragment 的 add 方法。 但是为了演示 add 和的方法的作用,示例中就同时 add 进去了。 由于两个 Fragment 都执行了 onResume 和 onCreateView 方法,那么我们可以得知,这两个碎片的 view 都已经被创建,并添加到 containView中了。

那么当 Activity 的时候变为用户不可见的时候,也就是用户跳转到新的界面,执行了 onPause 方法的时候两个 Fragment 的生命周期又是什么样子呢?

E/Fragment1: onPause
E/Fragment2: onPause
           

可以看到,两者也先后执行了onPause方法,这也充分说明了碎片的生命周期与宿主 Activity 保持一致。

调用一个 Fragment hide 方法的时候会发生什么事情呢? 我之前认为是两个 Fragment ,分别如下方的生命周期所示:

E/Fragment1: onPause
E/Fragment2: onResume
           

但是不是这样的?通过打印发现,Fragment没有走任何生命周期方法(上述生命周期方法,并不包括onHiddenChange方法)也许你会感到奇怪,但是这也从侧面证明了,如果 Fragment 已经被添加到 Activity 的之后,就会伴随Activity 的生命周期执行自己的生命周期。

第二种方案:replace 管理方法

要想知道 replace 方法,首先要了解 remove 方法。 那么我们修改一下之前添加方法,我们设置点击 Tab 的时候使用 replace 方法来添加和替换两个 Fragment。

E/Fragment0: onAttach
E/Fragment0: onCreate
E/Fragment0: onCreateView
E/Fragment0: onViewCreated
E/Fragment0: onActivityCreated
E/Fragment0: onViewStateRestored
E/Fragment0: onStart
E/Fragment0: onResume
E/Fragment0: onPause
E/Fragment0: onStop
E/Fragment0: onDestroyView
E/Fragment0: onDestroy
E/Fragment0: onDetach

E/Fragment1: onAttach
E/Fragment1: onCreate
E/Fragment1: onCreateView
E/Fragment1: onViewCreated
E/Fragment1: onActivityCreated
E/Fragment1: onViewStateRestored
E/Fragment1: onStart
E/Fragment1: onResume
           

通过log打印可以看出被替换的replace走了一个完整的生命周期(在不使用回退栈的情况下),也就是说此时 Fragment0 已经被销毁。完全被 Fragment1 替换。可能由于初学者都是学的 replace 来管理,所以惯性的认为 Fragment 的生命周期都是这么走。replace 方法比较好理解,这里就不多赘述了。

通过 onHiddenChanged 方法来管理 hide show 方法控制的 Fragment

通过上述生命周期分析,我们知道当调用 hide show 方法,是不会走 Fragment 任何生命周期方法,仅仅是Fragment的View被显示或者​隐藏。 那么我们应该如何在对应的 Fragment 显示时做对应的事情的呢?答案是有的,那就是 onHiddenChanged() 方法。

/**
     * Called when the hidden state (as returned by {@link #isHidden()} of
     * the fragment has changed.  Fragments start out not hidden; this will
     * be called whenever the fragment changes state from that.
     * @param hidden True if the fragment is now hidden, false otherwise.
     */
    public void onHiddenChanged(boolean hidden) {
    }
           

咦? 这里边也没有参数呀,看起来像一个 set 方法, 其实不是的。从参数说明可以看出 hidden参数就标志着对应的状态。

所以我们项目中的问题也可以迎刃而解了:

public void onHiddenChanged(boolean hidden) {
      if (hidden) {
            // 释放资源 
        } else {
            // 替换Activity 左上角应该显示的图标
        }    
    }
           

总结

我们知道 java 中频繁创建对象会很消耗性能,所以如果页面复杂使用 replace 来管理,会很影响产品性能。这里推荐使用 hide 和 show 方法来管理。在需要的时候,比如第一次点击 tab 的时候调用 add 方法添加对应的 Fragment 进入,然后点击对应tab 调用对应 Fragment 的 show 方法,并隐藏其他 tab 所对应的 Fragment。 但是这时候,我们不能使用 onResume 方法来管理,对应的 Tiltle 了 ,需要借助 onHiddenChanged 方法来管理。

另外需要注意如果我们使用 ViewPager 来管理多个 Fragment 的时候,都知道有两个Adapter供我们选择 一个是 「FragmentStatePagerAdapter」 另个一是 「FragmentPagerAdapter」,查看源码可知,前者通过 hide 和 show 来控制 Fragment 显示隐藏,后者通过 replace 来管理。由此可知对于页面复杂的情况用 FragmentStatePagerAdapter 更合适。

参考

Develop API Guides 片段

FragmentStatePagerAdapter