Android Fragment复用的那些事儿

约定

  • 如未特殊说明,本文中的知识点适用于 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,层层递进,直到全部恢复

复用的好处:

  • 避免显示错乱

  • 避免重复添加

  • 避免多余的内存占用

  • 优化界面启动速度

  • ........

所以复用还是相当有必要的,同时当我们知道了要复用的根本原因之后,如何复用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](https://developer.android.google.cn/topic/libraries/architecture/)(https://developer.android.google.cn/topic/libraries/architecture/viewmodel) 中的 (https://developer.android.google.cn/topic/libraries/architecture/viewmodel)[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]()(https://developer.android.google.cn/topic/libraries/architecture/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 没有表示出来,但不代表不存在

图片描述

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

  • 当存储状态的时候我们通过putFragment 记录下 FragmentA 的 mIndex, 使用的key 为字符串 "fragment:A"

  • 当我们需要查找 A 的时候,先根据 字符串 "fragment:A"(putFragment时使用的值) 去 bundle 中查出我们在 fragmentManager 销毁前记录的 mIndex = 5

  • 通过 mActivie 中得到 key = 5 的Fragment对象 即:Fragment A

  • 由于 fragment.mIndex 和 FragmentManagerImpl.mActive 无法访问到所以才需要 getFragment 和 putFragment。

注意事项:

  • getFragment 和 putFragment 必须成对使用。

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

七、总结

  • 在写 Activity 和 Fragment 的代码时区分区分新建和恢复,在恢复的情况下先查找 Fragment,找不到再创建实例对象

  • FM.getFragment 适合多个 Fragment 共用一个 ViewGroup 同时还无法为Fragment指定Tag的情况(如ViewPager)

  • FM.findFragmentById 适合一个 ViewGroup 对应 一个 Fragment 的情况

  • FM.findFragmentByTag 适合大多数情况,但需要在 add/replace 的时候为每个 Fragment 指定不同 tag

  • 当有多个 Fragment 对象具有相同的 tag 时,通过 findFragmentByTag 得到的是最后被添加的 Fragment

  • 当有多个 Fragment 对象共用同意个ViewGroup时,通过 findFragmentById 得到的是最后被添加的 Fragment

  • putFragment 使用时先判断 Fragment 是否已经添加到 FragmentManager

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

图片描述

最后给大家分享一份移动架构大纲,包含了移动架构师需要掌握的所有的技术体系,大家可以对比一下自己不足或者欠缺的地方有方向的去学习提升;

需要高清架构图以及图中视频资料的可以加入我的技术交流群:457848807私聊群主小姐姐免费获取

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

推荐阅读更多精彩内容