你真的会用Fragment吗?Fragment复用的那些事儿

本文的主要目的介绍的是当使用ViewPager时如何查找Fragment的办法,同时介绍一下在使用Fragment时的一些注意事项,以及几种查找方法所适用的场景。
作者: @怪盗kidou
如需转载不得删除本文中的任何内容(含本段)
如果博客中有不恰当之处欢迎在原文中留言交流
https://www.jianshu.com/p/31f013df7580

大家好,好像距离上次发布博客好像又过去了大半年了(额,好像每次发博客都有这句话),不过还好我的博客从来不是以数量取胜。

我统计了一下:截止到2018年5月23号,只有11篇文章的博客访问量已经超过 63 万了!感谢大家的支持!

好的屁话不多说,继续看文章

约定

  • 如未特殊说明,本文中的知识点适用于 Activity 重建的时候,即:
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState)
    // 略........
    if (savedInstanceState != null) {
        // 本文讨论的情况
    } else {
        // 非本文讨论的情况
    }
    // 略........
}
  • 为减少不必要的代码,文章中的 fmFM 均指代 FragmentManager
  • 如果你已经能熟练的使用 findFragmentById、findFragmentByTag、putFragment、getFragment 的用法以及它们各自的使用场景那么本文可能并不适合你

概述

  • 为什么要复用Fragment
  • 为何避免使用 FM.getFragments
  • FragmentManager.findFragmentById 的使用
  • FragmentManager.findFragmentByTag 的使用
  • ViewPager 复用之 FragmentManager.getFragment 的使用

一、 为什么要复用Fragment

根本原因只有一个:Activity 在重建的时候会恢复其包含的 FragmentManager ,FragmentManager 又会恢复其管理的 Fragment ,同理 Fragment 也会恢复其包含的 FragmentManager,层层递进,直到全部恢复

复用的好处:

  1. 避免显示错乱
  2. 避免重复添加
  3. 避免多余的内存占用
  4. 优化界面启动速度
  5. ........

所以复用还是相当有必要的,同时当我们知道了要复用的根本原因之后,如何复用Fragment也就变成 【如何查找已存在的Fragment】的问题了。


二、如何获取已经存在的Fragment

目前我知道的方法如下:

  • 【不推荐】获取全部的已添加到 FragmentManager 的
FragmentManager.getFragments()
  • 根据 TAG 查找 Fragment
FragmentManager.findFragmentByTag(String tag)
  • 根据 Id 查找 Fragment
FragmentManager.findFragmentById(int id)
  • 【重点】根据 Key 查找 Fragment,这个适合与 ViewPager 配合
FragmentManager.getFragment(Bundle bundle,String key)
FragmentManager.putFragment(Bundle bundle, String key, Fragment fragment)

三、谨慎使用FragmentManager.getFragments() 方法

既然不推荐,那总是有原因的,在这个小节会花费比较大的篇幅,我会结合代码告诉你为什么不推荐。

理由一:内容不可控导致Crash

FragmentManager.getFragments() 会返回所有已经添加到 FragmentManager 中的 Fragment,这就可能导致这个列表中包含了非我们自己所定义的Fragment,你可能会有疑问界面上不就显示我自己定义的Fragment么?

首先我们应该清楚的认识到 Fragment 不单单是界面的载体,它也可以用来实现别的功能,比如 生命周期 的监听。比如图片加载库 Glide 以及 Android 最新的 Android 架构组件 中的 ViewModel 都采用了这种方式。

所以如果我们的 Fragment 是和 ViewPager组合使用并且直接将包含这些实例对象(比如 ViewModel 用到 HolderFragment) FragmentManager.getFragments() 的结果丢给 FragmentPagerAdapter 的话那么就会达成本博客的第一项成就:Fragment重复添加

throw new IllegalStateException("Fragment already added: " + fragment)

理由二:顺序不可控

下面的这段代码我相信大家都很熟悉,就算自己没有写过也看别人写过

MainFragment mainFragment = (MainFragment) fm.getFragments().get(0)
// 略.......
SecondaryFragment secondaryFragment = (SecondaryFragment) fm.getFragments().get(1)
// 略.......

这样的写法就会帮助你达成第二项成就:类型转换异常

throw new ClassCastException("Cannot cast android.arch.lifecycle.HolderFragment to MainFragment")

ViewModel相关源码那里可以知道FragmentManager.getFragments() 中包含了其他的Fragment,而这些Fragment的位置往往是不固定,以ViewModel为例,HolderFragment的位置是由初始化的时机决定的。

也就是说你调整了一下 ViewModel 初始化的调用顺序或者在Kotlin项目中将 lateinit 改成了 by lazy 都可能会发生这样的Crash!就 lateinit 改成 by lazy 这条就是我前不久在做项目时真实遇到的。

理由三:26.x.y 版本中行为发生变更

在 版本25 中 Activity 是新建的情况下 返回的是 null ,在版本26中返回的是 Collections.EmptyList() ,前面我在维护公司项目时引入了 ROOM 然后有几个界面崩溃了!

此刻我的心情

经过排除发现而问题就出在下面的这段代码中。

mFragments = new ArrayList<>();
if(fm.getFragments() == null){
    mFragments.add(new MainFragment())
    mFragments.add(new SecondaryFragment())
}else{
    mFragments.addAll(fm.getFragments())
}
mViewPager.setAdapter(new MyViewPagerAdapter(fm, mFragments))
mTabLayout.setupWithViewPager(mViewPager)
// .....
mTabLayout.getTabAt(0).setText("MainFragment")
// .....

原因就是版本26下,返回的不是 null 导致 mFragments 是空的,自然mTabLayout里面是没有Tab的,所以导致了 空针异常,如果这段代码不依赖 getFragments 方法的话其实是没有问题的。

不知道大家有没有注意,如果这个Activity也使用ViewModel,那么还可能会顺带达成上面的 成就一和成就二

扎心了老铁

通过上面的一些例子我们知道了既然直接通过 FM.getFragments() 不可靠,那么通过其他几种方式来获取我们想要找的 Fragment 实例结果如何呢,接着往下看。


四、FM.findFragmentById()

该方法是用过 Fragment 所在的 ViewGroup 的 id(containerViewId) 来查找 Fragment,适合一个 ViewGroup 中只有一个 Fragment 的情况。

方法签名:

public abstract Fragment findFragmentById(@IdRes int id);

用法示例:

private MainFragment mainFragment;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (savedInstanceState != null) {
        mainFragment = (MainFragment) getSupportFragmentManager()
                // 这个ID和下面添加 fragment 时指定的 id 要一致
                .findFragmentById(android.R.id.content);
    } else {
        mainFragment = new MainFragment();
        getSupportFragmentManager().beginTransaction()
                .add(android.R.id.content, mainFragment)
                .commit();
    }
}

:

  • 该方式比较适合 ViewGroup 和 Fragment 是一对一的情况下使用,当不满足该条件时可以使用后面介绍的 findFragmentByTag 方法。
  • 当 一个 ViewGroup 中 有多个 Fragment 时该方法会返回最后添加到该 ViewGroup 的 Fragment。

五、FM.findFragmentByTag()

当一个 ViewGroup 中有多个 Fragment 时 findFragmentById 可能就不是太好使了,这种情况下就需要我们使用 findFragmentByTag 了。

由于是通过 tag 查找已经添加到 FragmentManager 里的 Fragment 实例对象,所以和 containerViewId 也就没有关系了,当然了在我们添加 Fragment 的时候也要注意给 fragment 指定 tag。

方法签名:

public abstract Fragment findFragmentByTag(String tag);

用法示例:

private MainFragment mainFragment;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (savedInstanceState != null) {
        mainFragment = (MainFragment) fm.findFragmentByTag(MainFragment.TAG);
    } else {
        mainFragment = new MainFragment();
        fm.beginTransaction()
                // 在添加的时候给其制定 tag,不然到时候上面的语句就没用了
                .add(android.R.id.content, mainFragment, MainFragment.TAG)
                .commit();
    }
}

上面就是一个很简单的用 TAG 来获取Fragment 的例子,这里需要注意的就是 tag 参数是我们在进行 addreplace 操作的时候指定的。

提示:

  • tag 是可以重复的,因为该参数的之只是 Fragment 的一个成员变量,只是我们无法访问(访问权限 default)。
  • 该方法总是返回 FragmentManager 中和该 tag 一致的最后一个 Fragment。也就是说如果有多个 Fragment 对象使用了同一个 tag 那么最后一个被添加的会被返回,所以不要为不同的 Fragment 对象指定相同的 tag。
  • 不要为同一个 Fragment 实例对象指定在不同的操作中指定不同的 tag,不然会抛出异常,当然这种情况一般是发生在重复添加的情况下

六、与 ViewPager 配合时不要试图使用 FM.findFragmentByTag

上面的 findFragmentByIdfindFragmentByTag 在使用的时候其实都是有一些隐藏限制的:

  • findFragmentById 适用于一个萝卜一个坑的情况
  • findFragmentByTag 使用于 可以指定为 Fragment 指定 tag 情况。

但是很不巧 ViewPager 与这两个情况都匹配不上,原因:

  • 由 ViewPager 所管理的 Fragment 使用的都是同一个 id ,即 ViewPager 的id。
  • 由于 ViewPager 来管理 Fragment 所以我们无法干预其添加移除的过程,所以没有办法为 fragment 指定 tag。

这次针对 ViewPager 的这种情况我要介绍的方法是 FragmentManager.getFragment()方法,与其配套使用的还有一个 FragmentManager.putFragment()方法。

你去搜 【ViewPager find fragment】 可能别人告诉你的 调用 makeFragmentName 生成 tag 或者用 findFragmentByTag("android:switcher:" + viewPager.getId() + ":" + viewPager.getCurrentItem()) 的那些做法就不要再用了!

// FragmentPagerAdapter.java
private static String makeFragmentName(int viewId, long id) {
    return "android:switcher:" + viewId + ":" + id;
}

正确的处理姿势示范:

private MainFragment mainFragment;
private SecondaryFragment secondaryFragment;

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (savedInstanceState != null) {
        mainFragment = (MainFragment) fm.getFragment(savedInstanceState, MainFragment.TAG);
        secondaryFragment = (SecondaryFragment) fm.getFragment(savedInstanceState, SecondaryFragment.TAG);
    }
    if (mainFragment == null) {
        mainFragment = new MainFragment();
    }
    if(secondaryFragment == null){
        secondaryFragment = new SecondaryFragment()
    }
    // ViewPager 的相关操作
}

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    if (mainFragment.isAdded()) {
        fm.putFragment(outState, MainFragment.TAG, mainFragment);
    }
    if (secondaryFragment.isAdded()) {
        fm.putFragment(outState, SecondaryFragment.TAG, secondaryFragment);
    }
}

两个方法的源码如下:

// FragmentManager.java,摘自版本 27.1.1
@Override
public void putFragment(Bundle bundle, String key, Fragment fragment) {
    if (fragment.mIndex < 0) { // 没有被添加到 FragmentManager
        throwException(new IllegalStateException("Fragment " + fragment
                + " is not currently in the FragmentManager"));
    }
    bundle.putInt(key, fragment.mIndex);
}

@Override
public Fragment getFragment(Bundle bundle, String key) {
    int index = bundle.getInt(key, -1);
    if (index == -1) {
        return null;
    }
    Fragment f = mActive.get(index);
    if (f == null) {
        throwException(new IllegalStateException("Fragment no longer exists for key "
                + key + ": index " + index));
    }
    return f;
}

原理解析:

先放两张图,然后结合图片解析

Fragment 在 FragmentManager 中的存储形式

上图只是给出了我们已经知道的,未知的 Fragment 没有表示出来,但不代表不存在

getFragment、putFragment.jpg

以 图中 Fragment A 为例,其他的同理

  1. 当存储状态的时候我们通过putFragment 记录下 FragmentA 的 mIndex, 使用的key 为字符串 "fragment:A"
  2. 当我们需要查找 A 的时候,先根据 字符串 "fragment:A"(putFragment时使用的值) 去 bundle 中查出我们在 fragmentManager 销毁前记录的 mIndex = 5
  3. 通过 mActivie 中得到 key = 5 的Fragment对象 即:Fragment A
  4. 由于 fragment.mIndex 和 FragmentManagerImpl.mActive 无法访问到所以才需要 getFragment 和 putFragment。

注意事项:

  • getFragment 和 putFragment 必须成对使用。
  • 在调用 putFragment 方法之前先保证该 fragment 是否已经添加到 FragmentManager 了(即fragment.mIndex >= 0),不然从源码可以得知会抛出异常。

七、总结

  1. 在写 Activity 和 Fragment 的代码时区分区分新建和恢复,在恢复的情况下先查找 Fragment,找不到再创建实例对象
  2. FM.getFragment 适合多个 Fragment 共用一个 ViewGroup 同时还无法为Fragment指定Tag的情况(如ViewPager)
  3. FM.findFragmentById 适合一个 ViewGroup 对应 一个 Fragment 的情况
  4. FM.findFragmentByTag 适合大多数情况,但需要在 add/replace 的时候为每个 Fragment 指定不同 tag
  5. 当有多个 Fragment 对象具有相同的 tag 时,通过 findFragmentByTag 得到的是最后被添加的 Fragment
  6. 当有多个 Fragment 对象共用同意个ViewGroup时,通过 findFragmentById 得到的是最后被添加的 Fragment
  7. putFragment 使用时先判断 Fragment 是否已经添加到 FragmentManager

最后附上一张图告诉你如何选择合适的方法来查找Fragment


查找Fragment方法选择.jpg

我最近刚刚开通了微信公众号(怪盗kidou),欢迎关注

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343