Android 保存 Fragment 引用及 getActivity() 爲空問題

問題

作 Android 應用開發的小夥伴們大多都被 Fragment 坑過. 最近研究了其中常見的一種坑, 記錄下來, 以避免遺忘. 問題大致是這樣的:
有時咱們但願在 Activity 中保存所建立的 Fragment 的引用, 以便後續邏輯中作界面更新等操做. 若是頁面中的 Fragment 都是靜態的 (不會被 remove, hide 等), 則通常不會出啥問題. 若是是多個 Fragment 切換的場景, 就容易出現 getActivity() 爲 null 等問題. 這種問題在使用 FragmentPagerAdapter 時尤爲容易出現.
這裏涉及兩個問題: Fragment 的建立和 Fragment 引用的保存. 兩個問題都有坑.java

先放結論 (編程建議):android

  1. 不要在 Activity.onCreate() 中直接 new Fragment(). Fragment 的建立應儘可能歸入 FragmentManager 的管理.
  2. 儘可能不要保存 Fragment 的引用. 在須要直接調用 Fragment 時, 使用 FragmentManager.findFragmentByTag() 等方法獲取相關 Fragment 的引用.
  3. 若是必定要保存 Fragment 引用, 則要謹慎選擇獲取引用的節點.

緣由分析

以一段實際代碼說明.
遇到主頁須要左右滑動切換標籤頁的需求, 最經常使用的就是 ViewPager + FragmePagerAdapter 方案了. 不少小夥伴可能會這樣寫 (示例代碼1):編程

public class TabChangeActivity extends AppCompatActivity {

    private ArrayList<Fragment> mFragmentList;
    private ViewPager mViewPager;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_tab_fragment_sample);
        mFragmentList = new ArrayList<>(3);
        mFragmentList.add(new Fragment1());
        mFragmentList.add(new Fragment2());
        mFragmentList.add(new Fragment3());
        mViewPager = (ViewPager) findViewById(R.id.view_pager);
        mViewPager.setAdapter(new SlidePagerAdapter(getSupportFragmentManager()));
    }

    private class SlidePagerAdapter extends FragmentPagerAdapter {

        public SlidePagerAdapter(FragmentManager fm) {
            super(fm);
        }

        @Override
        public Fragment getItem(int position) {
            return mFragmentList.get(position);
        }

        @Override
        public int getCount() {
            return mFragmentList.size();
        }
    }
}

上例是一個最簡單的標籤頁切換界面寫法, 佈局中只有一個 ViewPager, 就再也不貼出了.
但這段代碼是存在隱患的.
這裏首先複習一下 Activity 管理 Fragment 的方式. 在代碼中動態顯示 Fragment 時, 大致流程以下:數組

private void showFragment1() {
    FragmentManager fragmentManager = getSupportFragmentManager();
    FragmentTransaction transaction = fragmentManager.beginTransaction();
    // 查看 fragment1 是否已經被添加
    Fragment1 fragment1 = (Fragment1) fragmentManager.findFragmentByTag("fragment1");
    if (fragment1 == null) {
        // fragment1 還沒有被添加, 則建立並添加
        fragment1 = new Fragment1();
        transaction.add(R.id.submitter_fragment_container, fragment1, "fragment1");
    } else {
        // fragment1 已被添加, 則調用 show() 方法讓其顯示
        transaction.show(fragment1);
    }
    transaction.commit();
}

但 示例代碼1 中並無相似邏輯. 實際上是被 FragmentPagerAdapter 封裝了, 但邏輯依然是同樣的:
FragmentPagerAdapter 在須要展現 fragment1 時, 會首先嚐試經過 FragmentManager.findFragmentByTag() 找到它. 若是找不到, 纔會調用 FragmentPagerAdapter.getItem() 來建立它.app

回到 示例代碼1, 在正常狀況下, 這段代碼是能夠完美運行的. 但若是咱們的界面被系統回收掉了, 當用戶再次返回這個界面時, 問題就來了. 在這種狀況下:ide

  • 由於 Activity 被銷燬了, 所以 onCreate() 會被調用, 咱們的三個 Fragment 會被從新建立並裝入 mFragmentList 數組.
  • 又由於 Activity 被銷燬了, 所以系統會自動恢復界面狀態, 包括以前已經被添加的 Fragment. 恢復完成後, 輪到 FragmentPagerAdapter 顯示 fragment1. FragmentPagerAdapter 經過 FragmentManager.findFragmentByTag(), 發現 fragment1 已經被添加了 (被添加的爲老 Fragment, 即被系統恢復的那個). 所以不會再去調用 FragmentPagerAdapter.getItem(), 所以 FragmentPagerAdapter 直接顯示了被系統恢復出來的 fragment1.

沒錯, 這種狀況下, Fragment1 在 Activity 中其實有兩個實例:
一個是真正的被 Activity 添加並顯示的實例;
一個是在 onCreate() 中被建立, 並保存在 mFragmentList 中的沒有什麼卵用的實例.佈局

能夠想見, 這種狀態下確定會出現不少莫名其妙的問題, 其中就包括 getActivity() 返回 null 的問題.post

吐槽: FragmentPagerAdapter.getItem() 方法明明就是 FragmentPagerAdapter 用來內部建立 Fragment 用的啊, 根本不是用來供外部獲取 Fragment 用的. 若是更名叫 createItem() 或者 createFragment() 之類的, 估計能夠防止很多人掉坑的.

代碼修正

基於以上分析可知, 在 Activity.onCreate() 中建立 Fragment 是不恰當的. 應該把 Fragment 的建立放在 FragmentPagerAdapter.getItem() 中. 通過改進的 示例代碼1 以下:this

public class TabChangeActivity extends AppCompatActivity {

    private ViewPager mViewPager;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_tab_fragment_sample);
        mViewPager = (ViewPager) findViewById(R.id.view_pager);
        mViewPager.setAdapter(new SlidePagerAdapter(getSupportFragmentManager()));
    }

    private class SlidePagerAdapter extends FragmentPagerAdapter {

        public SlidePagerAdapter(FragmentManager fm) {
            super(fm);
        }

        @Override
        public Fragment getItem(int position) {
            switch (position) {
                case 0:
                    return new Fragment1();
                case 1:
                    return new Fragment2();
                case 2:
                    return new Fragment3();
                default:
                    return null; // unlikely to happen
            }
        }

        @Override
        public int getCount() {
            return 3;
        }
    }
}

即: 再也不用 mFragmentList 保存各個 Fragment 的引用了, Fragment 的建立徹底交給 FragmentPagerAdapter 去作.
其實在其餘的使用 Fragment 的場景中, 也會出現上述問題, 也應該遵循一樣的原則, 即文章開頭所列的 建議1 和 建議2 .code

這樣是解決了上面提到的 Activity 銷燬恢復的問題, 但若是咱們在 Activity 邏輯中, 必定要取到 Fragment 引用, 該怎麼辦呢. (好比, 點擊 ActionBar 上的按鈕則改變 Fragment 中的某段文字).
有兩種方法能夠解決保存 Fragment 引用的問題.

保存引用

如前所述, 確定不能用 FragmentPagerAdapter.getItem() 方法來獲取!
要找到合適的方法, 須要瞄一眼源碼. FragmentPagerAdapter 的源碼至關的短:

public abstract class FragmentPagerAdapter extends PagerAdapter {

    ......

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }

        final long itemId = getItemId(position);

        // Do we already have this fragment?
        String name = makeFragmentName(container.getId(), itemId);
        Fragment fragment = mFragmentManager.findFragmentByTag(name);
        if (fragment != null) {
            if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
            mCurTransaction.attach(fragment);
        } else {
            fragment = getItem(position);
            if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
            mCurTransaction.add(container.getId(), fragment,
                    makeFragmentName(container.getId(), itemId));
        }
        if (fragment != mCurrentPrimaryItem) {
            fragment.setMenuVisibility(false);
            fragment.setUserVisibleHint(false);
        }

        return fragment;
    }

    ......

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

上面只列出了其中的兩個關鍵方法:
instantiateItem() 方法是負責建立 pager 頁的方法, 其邏輯就是先判斷 Fragment 是否存在, 存在則顯示, 不存在則調用 getItem(position) 建立.
makeFragmentName() 方法用來爲一個特定位置的 fragment 生成一個 tag, 規則就是容器 ViewGroup 的 id 和 Fragment 位置的組合. 其中 ViewGroup 的 id 就是 ViewPager 在 Activity 界面中的 id.

所以取到 Fragment 引用的方法也就找到了:

方法一

既然咱們都知道 tag 的生成規則了, 找到 Fragment 那還不是 so easy.
仍是以上面的 示例代碼1 爲例, 獲取 fragment1 的引用, 這麼作就能夠了:

private void changeFragment1Text() {
    String tag = "android:switcher:" + R.id.view_pager + ":" + 0;
    Fragment1 fragment1 = (Fragment1) getSupportFragmentManager().findFragmentByTag(tag);
    // 必定要作判空, 由於你要找的 Fragment 這時可能尚未加入 Activity 中.
    if (fragment1 != null) {
        fragment1.setText("Laziness is a programmer's feature.");
    } else {
        Log.e("lyux", "fragment not added yet.");
    }
}

這種方法有兩個缺點:
一是, tag 的規則依賴一個源碼中的私有方法, 谷歌大大哪天不爽要改了這條規則, 咱們的程序就會出錯了.
二是, 對於另外一個裝載 Fragment 的 PagerAdapter, 即 FragmentStatePagerAdapter, 這個方法是不適用的.

FragmentStatePagerAdapter 是爲了懶加載及頁面回收的目的而編寫的, 即不把每一個 page 頁的內容都保存在內存裏. 所以它在建立了 Fragment 後, 沒有給其附加 tag. 因此由它建立的 Fragment 沒法用 FragmentManager.findFragmentByTag() 方法找到. 具體見其源碼, 也不長.

方法二

還有一種思路, 是重載 FragmentPagerAdapter 類中的 instantiateItem() 方法, 獲得 Fragment 引用. 依然以 示例代碼1 爲例, 將 SlidePagerAdapter 作以下改寫便可:

public class TabChangeActivity extends AppCompatActivity {

    private ViewPager mViewPager;
    private Fragment1 mFragment1;
    private Fragment2 mFragment2;
    private Fragment3 mFragment3;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_tab_fragment_sample);
        mViewPager = (ViewPager) findViewById(R.id.view_pager);
        mViewPager.setAdapter(new SlidePagerAdapter(getSupportFragmentManager()));

        // 延遲5秒改變文字. 若是馬上執行, mFragment1 確定是 null.
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                if (mFragment1 != null) {
                    mFragment1.setText("Every program must have a purpose. If not, it is deleted. -- The Matrix");
                }
            }
        }, 5000);
    }

    private class SlidePagerAdapter extends FragmentPagerAdapter {

        public SlidePagerAdapter(FragmentManager fm) {
            super(fm);
        }

        @Override
        public Fragment getItem(int position) {
            switch (position) {
                case 0:
                    return new Fragment1();
                case 1:
                    return new Fragment2();
                case 2:
                    return new Fragment3();
                default:
                    return null; // unlikely to happen
            }
        }

        @Override
        public int getCount() {
            return 3;
        }

        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            Fragment fragment = (Fragment) super.instantiateItem(container, position);
            switch (position) {
                case 0:
                    mFragment1 = (Fragment1) fragment;
                    break;
                case 1:
                    mFragment2 = (Fragment2) fragment;
                    break;
                case 2:
                    mFragment3 = (Fragment3) fragment;
                    break;
            }
            return fragment;
        }
    }
}

由於 instantiateItem() 方法管理了 Fragment 的建立及重用, 所以不管其是新建立的, 仍是被恢復的, 均可以正確取到引用.

注意: 不要在 FragmentStatePagerAdapter 場景中使用該方法. 由於咱們保存了每一頁的 Fragment 的引用, 就會阻止其被回收, 那 FragmentStatePagerAdapter 就白用了: 不就是爲了能夠回收頁面才用它的嘛.
真要用的話就用 WeakReference<Fragment> 保存其弱引用. 但聽說 4.0 後的 Android 虛擬機中弱引用等於沒引用, 會很快被回收掉. (這句是聽一位虛擬機大牛說的)
相關文章
相關標籤/搜索