Android Fragment復用的那些事兒 [復制鏈接]

2019-6-6 09:57
EmailLi 閱讀:446 評論:1 贊:0
Tag:  

約定

  • 如未特殊說明,本文中的知識點適用于 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
圖片描述


我來說兩句
您需要登錄后才可以評論 登錄 | 立即注冊
facelist
所有評論(1)
天明向日葵 2019-6-10 09:55
學習了
回復
領先的中文移動開發者社區
18620764416
7*24全天服務
意見反饋:[email protected]

掃一掃關注我們

Powered by Discuz! X3.2© 2001-2019 Comsenz Inc.( 粵ICP備15117877號 )

海南特区七星彩