本文的主要目的介紹的是當(dāng)使用ViewPager時(shí)如何查找Fragment的辦法,同時(shí)介紹一下在使用Fragment時(shí)的一些注意事項(xiàng),以及幾種查找方法所適用的場(chǎng)景。
作者: @怪盜kidou
如需轉(zhuǎn)載不得刪除本文中的任何內(nèi)容(含本段)
如果博客中有不恰當(dāng)之處歡迎在原文中留言交流
http://www.itdecent.cn/p/31f013df7580
大家好,好像距離上次發(fā)布博客好像又過(guò)去了大半年了(額,好像每次發(fā)博客都有這句話),不過(guò)還好我的博客從來(lái)不是以數(shù)量取勝。
我統(tǒng)計(jì)了一下:截止到2018年5月23號(hào),只有11篇文章的博客訪問(wèn)量已經(jīng)超過(guò) 63 萬(wàn)了!感謝大家的支持!
好的屁話不多說(shuō),繼續(xù)看文章
約定
- 如未特殊說(shuō)明,本文中的知識(shí)點(diǎn)適用于 Activity 重建的時(shí)候,即:
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState)
// 略........
if (savedInstanceState != null) {
// 本文討論的情況
} else {
// 非本文討論的情況
}
// 略........
}
- 為減少不必要的代碼,文章中的
fm、FM均指代FragmentManager - 如果你已經(jīng)能熟練的使用 findFragmentById、findFragmentByTag、putFragment、getFragment 的用法以及它們各自的使用場(chǎng)景那么本文可能并不適合你
概述
- 為什么要復(fù)用Fragment
- 為何避免使用 FM.getFragments
- FragmentManager.findFragmentById 的使用
- FragmentManager.findFragmentByTag 的使用
- ViewPager 復(fù)用之 FragmentManager.getFragment 的使用
一、 為什么要復(fù)用Fragment
根本原因只有一個(gè):Activity 在重建的時(shí)候會(huì)恢復(fù)其包含的 FragmentManager ,F(xiàn)ragmentManager 又會(huì)恢復(fù)其管理的 Fragment ,同理 Fragment 也會(huì)恢復(fù)其包含的 FragmentManager,層層遞進(jìn),直到全部恢復(fù)
復(fù)用的好處:
- 避免顯示錯(cuò)亂
- 避免重復(fù)添加
- 避免多余的內(nèi)存占用
- 優(yōu)化界面啟動(dòng)速度
- ........
所以復(fù)用還是相當(dāng)有必要的,同時(shí)當(dāng)我們知道了要復(fù)用的根本原因之后,如何復(fù)用Fragment也就變成 【如何查找已存在的Fragment】的問(wèn)題了。
二、如何獲取已經(jīng)存在的Fragment
目前我知道的方法如下:
- 【不推薦】獲取全部的已添加到 FragmentManager 的
FragmentManager.getFragments()
- 根據(jù) TAG 查找 Fragment
FragmentManager.findFragmentByTag(String tag)
- 根據(jù) Id 查找 Fragment
FragmentManager.findFragmentById(int id)
- 【重點(diǎn)】根據(jù) Key 查找 Fragment,這個(gè)適合與 ViewPager 配合
FragmentManager.getFragment(Bundle bundle,String key)
FragmentManager.putFragment(Bundle bundle, String key, Fragment fragment)
三、謹(jǐn)慎使用FragmentManager.getFragments() 方法
既然不推薦,那總是有原因的,在這個(gè)小節(jié)會(huì)花費(fèi)比較大的篇幅,我會(huì)結(jié)合代碼告訴你為什么不推薦。
理由一:內(nèi)容不可控導(dǎo)致Crash
FragmentManager.getFragments() 會(huì)返回所有已經(jīng)添加到 FragmentManager 中的 Fragment,這就可能導(dǎo)致這個(gè)列表中包含了非我們自己所定義的Fragment,你可能會(huì)有疑問(wèn)界面上不就顯示我自己定義的Fragment么?
首先我們應(yīng)該清楚的認(rèn)識(shí)到 Fragment 不單單是界面的載體,它也可以用來(lái)實(shí)現(xiàn)別的功能,比如 生命周期 的監(jiān)聽(tīng)。比如圖片加載庫(kù) Glide 以及 Android 最新的 Android 架構(gòu)組件 中的 ViewModel 都采用了這種方式。
所以如果我們的 Fragment 是和 ViewPager組合使用并且直接將包含這些實(shí)例對(duì)象(比如 ViewModel 用到 HolderFragment) FragmentManager.getFragments() 的結(jié)果丟給 FragmentPagerAdapter 的話那么就會(huì)達(dá)成本博客的第一項(xiàng)成就:Fragment重復(fù)添加
throw new IllegalStateException("Fragment already added: " + fragment)
理由二:順序不可控
下面的這段代碼我相信大家都很熟悉,就算自己沒(méi)有寫(xiě)過(guò)也看別人寫(xiě)過(guò)
MainFragment mainFragment = (MainFragment) fm.getFragments().get(0)
// 略.......
SecondaryFragment secondaryFragment = (SecondaryFragment) fm.getFragments().get(1)
// 略.......
這樣的寫(xiě)法就會(huì)幫助你達(dá)成第二項(xiàng)成就:類型轉(zhuǎn)換異常
throw new ClassCastException("Cannot cast android.arch.lifecycle.HolderFragment to MainFragment")
從 ViewModel相關(guān)源碼那里可以知道FragmentManager.getFragments() 中包含了其他的Fragment,而這些Fragment的位置往往是不固定,以ViewModel為例,HolderFragment的位置是由初始化的時(shí)機(jī)決定的。
也就是說(shuō)你調(diào)整了一下 ViewModel 初始化的調(diào)用順序或者在Kotlin項(xiàng)目中將 lateinit 改成了 by lazy 都可能會(huì)發(fā)生這樣的Crash!就 lateinit 改成 by lazy 這條就是我前不久在做項(xiàng)目時(shí)真實(shí)遇到的。
理由三:26.x.y 版本中行為發(fā)生變更
在 版本25 中 Activity 是新建的情況下 返回的是 null ,在版本26中返回的是 Collections.EmptyList() ,前面我在維護(hù)公司項(xiàng)目時(shí)引入了 ROOM 然后有幾個(gè)界面崩潰了!

經(jīng)過(guò)排除發(fā)現(xiàn)而問(wèn)題就出在下面的這段代碼中。
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 導(dǎo)致 mFragments 是空的,自然mTabLayout里面是沒(méi)有Tab的,所以導(dǎo)致了 空針異常,如果這段代碼不依賴 getFragments 方法的話其實(shí)是沒(méi)有問(wèn)題的。
不知道大家有沒(méi)有注意,如果這個(gè)Activity也使用ViewModel,那么還可能會(huì)順帶達(dá)成上面的 成就一和成就二

通過(guò)上面的一些例子我們知道了既然直接通過(guò) FM.getFragments() 不可靠,那么通過(guò)其他幾種方式來(lái)獲取我們想要找的 Fragment 實(shí)例結(jié)果如何呢,接著往下看。
四、FM.findFragmentById()
該方法是用過(guò) Fragment 所在的 ViewGroup 的 id(containerViewId) 來(lái)查找 Fragment,適合一個(gè) ViewGroup 中只有一個(gè) 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()
// 這個(gè)ID和下面添加 fragment 時(shí)指定的 id 要一致
.findFragmentById(android.R.id.content);
} else {
mainFragment = new MainFragment();
getSupportFragmentManager().beginTransaction()
.add(android.R.id.content, mainFragment)
.commit();
}
}
:
- 該方式比較適合 ViewGroup 和 Fragment 是一對(duì)一的情況下使用,當(dāng)不滿足該條件時(shí)可以使用后面介紹的
findFragmentByTag方法。 - 當(dāng) 一個(gè) ViewGroup 中 有多個(gè) Fragment 時(shí)該方法會(huì)返回最后添加到該 ViewGroup 的 Fragment。
五、FM.findFragmentByTag()
當(dāng)一個(gè) ViewGroup 中有多個(gè) Fragment 時(shí) findFragmentById 可能就不是太好使了,這種情況下就需要我們使用 findFragmentByTag 了。
由于是通過(guò) tag 查找已經(jīng)添加到 FragmentManager 里的 Fragment 實(shí)例對(duì)象,所以和 containerViewId 也就沒(méi)有關(guān)系了,當(dāng)然了在我們添加 Fragment 的時(shí)候也要注意給 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()
// 在添加的時(shí)候給其制定 tag,不然到時(shí)候上面的語(yǔ)句就沒(méi)用了
.add(android.R.id.content, mainFragment, MainFragment.TAG)
.commit();
}
}
上面就是一個(gè)很簡(jiǎn)單的用 TAG 來(lái)獲取Fragment 的例子,這里需要注意的就是 tag 參數(shù)是我們?cè)谶M(jìn)行 add 或 replace 操作的時(shí)候指定的。
提示:
- tag 是可以重復(fù)的,因?yàn)樵搮?shù)的之只是 Fragment 的一個(gè)成員變量,只是我們無(wú)法訪問(wèn)(訪問(wèn)權(quán)限 default)。
- 該方法總是返回 FragmentManager 中和該 tag 一致的最后一個(gè) Fragment。也就是說(shuō)如果有多個(gè) Fragment 對(duì)象使用了同一個(gè) tag 那么最后一個(gè)被添加的會(huì)被返回,所以不要為不同的 Fragment 對(duì)象指定相同的 tag。
- 不要為同一個(gè) Fragment 實(shí)例對(duì)象指定在不同的操作中指定不同的 tag,不然會(huì)拋出異常,當(dāng)然這種情況一般是發(fā)生在重復(fù)添加的情況下
六、與 ViewPager 配合時(shí)不要試圖使用 FM.findFragmentByTag
上面的 findFragmentById 和 findFragmentByTag 在使用的時(shí)候其實(shí)都是有一些隱藏限制的:
- findFragmentById 適用于一個(gè)蘿卜一個(gè)坑的情況
- findFragmentByTag 使用于 可以指定為 Fragment 指定 tag 情況。
但是很不巧 ViewPager 與這兩個(gè)情況都匹配不上,原因:
- 由 ViewPager 所管理的 Fragment 使用的都是同一個(gè) id ,即 ViewPager 的id。
- 由于 ViewPager 來(lái)管理 Fragment 所以我們無(wú)法干預(yù)其添加移除的過(guò)程,所以沒(méi)有辦法為 fragment 指定 tag。
這次針對(duì) ViewPager 的這種情況我要介紹的方法是 FragmentManager.getFragment()方法,與其配套使用的還有一個(gè) FragmentManager.putFragment()方法。
你去搜 【ViewPager find fragment】 可能別人告訴你的 調(diào)用 makeFragmentName 生成 tag 或者用 findFragmentByTag("android:switcher:" + viewPager.getId() + ":" + viewPager.getCurrentItem()) 的那些做法就不要再用了!
// FragmentPagerAdapter.java
private static String makeFragmentName(int viewId, long id) {
return "android:switcher:" + viewId + ":" + id;
}
正確的處理姿勢(shì)示范:
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 的相關(guān)操作
}
@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);
}
}
兩個(gè)方法的源碼如下:
// FragmentManager.java,摘自版本 27.1.1
@Override
public void putFragment(Bundle bundle, String key, Fragment fragment) {
if (fragment.mIndex < 0) { // 沒(méi)有被添加到 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;
}
原理解析:
先放兩張圖,然后結(jié)合圖片解析

上圖只是給出了我們已經(jīng)知道的,未知的 Fragment 沒(méi)有表示出來(lái),但不代表不存在

以 圖中 Fragment A 為例,其他的同理
- 當(dāng)存儲(chǔ)狀態(tài)的時(shí)候我們通過(guò)putFragment 記錄下 FragmentA 的 mIndex, 使用的key 為字符串 "fragment:A"
- 當(dāng)我們需要查找 A 的時(shí)候,先根據(jù) 字符串 "fragment:A"(putFragment時(shí)使用的值) 去 bundle 中查出我們?cè)?fragmentManager 銷毀前記錄的 mIndex = 5
- 通過(guò) mActivie 中得到 key = 5 的Fragment對(duì)象 即:Fragment A
- 由于 fragment.mIndex 和 FragmentManagerImpl.mActive 無(wú)法訪問(wèn)到所以才需要 getFragment 和 putFragment。
注意事項(xiàng):
- getFragment 和 putFragment 必須成對(duì)使用。
- 在調(diào)用 putFragment 方法之前先保證該 fragment 是否已經(jīng)添加到 FragmentManager 了(即fragment.mIndex >= 0),不然從源碼可以得知會(huì)拋出異常。
七、總結(jié)
- 在寫(xiě) Activity 和 Fragment 的代碼時(shí)區(qū)分區(qū)分新建和恢復(fù),在恢復(fù)的情況下先查找 Fragment,找不到再創(chuàng)建實(shí)例對(duì)象
- FM.getFragment 適合多個(gè) Fragment 共用一個(gè) ViewGroup 同時(shí)還無(wú)法為Fragment指定Tag的情況(如ViewPager)
- FM.findFragmentById 適合一個(gè) ViewGroup 對(duì)應(yīng) 一個(gè) Fragment 的情況
- FM.findFragmentByTag 適合大多數(shù)情況,但需要在 add/replace 的時(shí)候?yàn)槊總€(gè) Fragment 指定不同 tag
- 當(dāng)有多個(gè) Fragment 對(duì)象具有相同的 tag 時(shí),通過(guò) findFragmentByTag 得到的是最后被添加的 Fragment
- 當(dāng)有多個(gè) Fragment 對(duì)象共用同意個(gè)ViewGroup時(shí),通過(guò) findFragmentById 得到的是最后被添加的 Fragment
- putFragment 使用時(shí)先判斷 Fragment 是否已經(jīng)添加到 FragmentManager
最后附上一張圖告訴你如何選擇合適的方法來(lái)查找Fragment

我最近剛剛開(kāi)通了微信公眾號(hào)(怪盜kidou),歡迎關(guān)注