作 Android 應用開發的小夥伴們大多都被 Fragment 坑過. 最近研究了其中常見的一種坑, 記錄下來, 以避免遺忘. 問題大致是這樣的:
有時咱們但願在 Activity 中保存所建立的 Fragment 的引用, 以便後續邏輯中作界面更新等操做. 若是頁面中的 Fragment 都是靜態的 (不會被 remove, hide 等), 則通常不會出啥問題. 若是是多個 Fragment 切換的場景, 就容易出現 getActivity() 爲 null 等問題. 這種問題在使用 FragmentPagerAdapter 時尤爲容易出現.
這裏涉及兩個問題: Fragment 的建立和 Fragment 引用的保存. 兩個問題都有坑.java
先放結論 (編程建議):android
new Fragment()
. Fragment 的建立應儘可能歸入 FragmentManager 的管理.以一段實際代碼說明.
遇到主頁須要左右滑動切換標籤頁的需求, 最經常使用的就是 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
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 虛擬機中弱引用等於沒引用, 會很快被回收掉. (這句是聽一位虛擬機大牛說的)