問(wèn)題
做 Android 應(yīng)用開(kāi)發(fā)的小伙伴們大多都被 Fragment 坑過(guò). 最近研究了其中常見(jiàn)的一種坑, 記錄下來(lái), 以免遺忘. 問(wèn)題大體是這樣的:
有時(shí)我們希望在 Activity 中保存所創(chuàng)建的 Fragment 的引用, 以便后續(xù)邏輯中做界面更新等操作. 如果頁(yè)面中的 Fragment 都是靜態(tài)的 (不會(huì)被 remove, hide 等), 則一般不會(huì)出啥問(wèn)題. 如果是多個(gè) Fragment 切換的場(chǎng)景, 就容易出現(xiàn) getActivity() 為 null 等問(wèn)題. 這種問(wèn)題在使用 FragmentPagerAdapter 時(shí)尤其容易出現(xiàn).
這里涉及兩個(gè)問(wèn)題: Fragment 的創(chuàng)建和 Fragment 引用的保存. 兩個(gè)問(wèn)題都有坑.
先放結(jié)論 (編程建議):
- 不要在 Activity.onCreate() 中直接
new Fragment()
. Fragment 的創(chuàng)建應(yīng)盡量納入 FragmentManager 的管理. - 盡量不要保存 Fragment 的引用. 在需要直接調(diào)用 Fragment 時(shí), 使用 FragmentManager.findFragmentByTag() 等方法獲取相關(guān) Fragment 的引用.
- 如果一定要保存 Fragment 引用, 則要謹(jǐn)慎選擇獲取引用的節(jié)點(diǎn).
原因分析
以一段實(shí)際代碼說(shuō)明.
遇到主頁(yè)需要左右滑動(dòng)切換標(biāo)簽頁(yè)的需求, 最常用的就是 ViewPager + FragmePagerAdapter 方案了. 很多小伙伴可能會(huì)這樣寫(xiě) (示例代碼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();
}
}
}
上例是一個(gè)最簡(jiǎn)單的標(biāo)簽頁(yè)切換界面寫(xiě)法, 布局中只有一個(gè) ViewPager, 就不再貼出了.
但這段代碼是存在隱患的.
這里首先復(fù)習(xí)一下 Activity 管理 Fragment 的方式. 在代碼中動(dòng)態(tài)顯示 Fragment 時(shí), 大體流程如下:
private void showFragment1() {
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
// 查看 fragment1 是否已經(jīng)被添加
Fragment1 fragment1 = (Fragment1) fragmentManager.findFragmentByTag("fragment1");
if (fragment1 == null) {
// fragment1 尚未被添加, 則創(chuàng)建并添加
fragment1 = new Fragment1();
transaction.add(R.id.submitter_fragment_container, fragment1, "fragment1");
} else {
// fragment1 已被添加, 則調(diào)用 show() 方法讓其顯示
transaction.show(fragment1);
}
transaction.commit();
}
但 示例代碼1 中并沒(méi)有類(lèi)似邏輯. 其實(shí)是被 FragmentPagerAdapter 封裝了, 但邏輯依然是一樣的:
FragmentPagerAdapter 在需要展示 fragment1 時(shí), 會(huì)首先嘗試通過(guò) FragmentManager.findFragmentByTag()
找到它. 如果找不到, 才會(huì)調(diào)用 FragmentPagerAdapter.getItem()
來(lái)創(chuàng)建它.
回到 示例代碼1, 在正常情況下, 這段代碼是可以完美運(yùn)行的. 但如果我們的界面被系統(tǒng)回收掉了, 當(dāng)用戶(hù)再次返回這個(gè)界面時(shí), 問(wèn)題就來(lái)了. 在這種情況下:
- 因?yàn)?Activity 被銷(xiāo)毀了, 因此 onCreate() 會(huì)被調(diào)用, 我們的三個(gè) Fragment 會(huì)被重新創(chuàng)建并裝入 mFragmentList 數(shù)組.
- 又因?yàn)?Activity 被銷(xiāo)毀了, 因此系統(tǒng)會(huì)自動(dòng)恢復(fù)界面狀態(tài), 包括之前已經(jīng)被添加的 Fragment. 恢復(fù)完成后, 輪到 FragmentPagerAdapter 顯示 fragment1. FragmentPagerAdapter 通過(guò)
FragmentManager.findFragmentByTag()
, 發(fā)現(xiàn) fragment1 已經(jīng)被添加了 (被添加的為老 Fragment, 即被系統(tǒng)恢復(fù)的那個(gè)). 因此不會(huì)再去調(diào)用FragmentPagerAdapter.getItem()
, 因此 FragmentPagerAdapter 直接顯示了被系統(tǒng)恢復(fù)出來(lái)的 fragment1.
沒(méi)錯(cuò), 這種情況下, Fragment1 在 Activity 中其實(shí)有兩個(gè)實(shí)例:
一個(gè)是真正的被 Activity 添加并顯示的實(shí)例;
一個(gè)是在 onCreate() 中被創(chuàng)建, 并保存在 mFragmentList 中的沒(méi)有什么卵用的實(shí)例.
可以想見(jiàn), 這種狀態(tài)下肯定會(huì)出現(xiàn)很多莫名其妙的問(wèn)題, 其中就包括 getActivity()
返回 null 的問(wèn)題.
吐槽:
FragmentPagerAdapter.getItem()
方法明明就是 FragmentPagerAdapter 用來(lái)內(nèi)部創(chuàng)建 Fragment 用的啊, 根本不是用來(lái)供外部獲取 Fragment 用的. 如果改名叫createItem()
或者createFragment()
之類(lèi)的, 估計(jì)可以防止不少人掉坑的.
代碼修正
基于以上分析可知, 在 Activity.onCreate()
中創(chuàng)建 Fragment 是不恰當(dāng)?shù)? 應(yīng)該把 Fragment 的創(chuàng)建放在 FragmentPagerAdapter.getItem()
中. 經(jīng)過(guò)改進(jìn)的 示例代碼1 如下:
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 保存各個(gè) Fragment 的引用了, Fragment 的創(chuàng)建完全交給 FragmentPagerAdapter 去做.
其實(shí)在其他的使用 Fragment 的場(chǎng)景中, 也會(huì)出現(xiàn)上述問(wèn)題, 也應(yīng)該遵循同樣的原則, 即文章開(kāi)頭所列的 建議1 和 建議2 .
這樣是解決了上面提到的 Activity 銷(xiāo)毀恢復(fù)的問(wèn)題, 但如果我們?cè)?Activity 邏輯中, 一定要取到 Fragment 引用, 該怎么辦呢. (比如, 點(diǎn)擊 ActionBar 上的按鈕則改變 Fragment 中的某段文字).
有兩種方法可以解決保存 Fragment 引用的問(wèn)題.
保存引用
如前所述, 肯定不能用 FragmentPagerAdapter.getItem()
方法來(lái)獲取!
要找到合適的方法, 需要瞄一眼源碼. FragmentPagerAdapter 的源碼相當(dāng)?shù)亩?
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;
}
}
上面只列出了其中的兩個(gè)關(guān)鍵方法:
instantiateItem()
方法是負(fù)責(zé)創(chuàng)建 pager 頁(yè)的方法, 其邏輯就是先判斷 Fragment 是否存在, 存在則顯示, 不存在則調(diào)用 getItem(position)
創(chuàng)建.
makeFragmentName()
方法用來(lái)為一個(gè)特定位置的 fragment 生成一個(gè) tag, 規(guī)則就是容器 ViewGroup 的 id 和 Fragment 位置的組合. 其中 ViewGroup 的 id 就是 ViewPager 在 Activity 界面中的 id.
因此取到 Fragment 引用的方法也就找到了:
方法一
既然我們都知道 tag 的生成規(guī)則了, 找到 Fragment 那還不是 so easy.
還是以上面的 示例代碼1 為例, 獲取 fragment1 的引用, 這么做就可以了:
private void changeFragment1Text() {
String tag = "android:switcher:" + R.id.view_pager + ":" + 0;
Fragment1 fragment1 = (Fragment1) getSupportFragmentManager().findFragmentByTag(tag);
// 一定要做判空, 因?yàn)槟阋业?Fragment 這時(shí)可能還沒(méi)有加入 Activity 中.
if (fragment1 != null) {
fragment1.setText("Laziness is a programmer's feature.");
} else {
Log.e("lyux", "fragment not added yet.");
}
}
這種方法有兩個(gè)缺點(diǎn):
一是, tag 的規(guī)則依賴(lài)一個(gè)源碼中的私有方法, 谷歌大大哪天不爽要改了這條規(guī)則, 我們的程序就會(huì)出錯(cuò)了.
二是, 對(duì)于另一個(gè)裝載 Fragment 的 PagerAdapter, 即 FragmentStatePagerAdapter
, 這個(gè)方法是不適用的.
FragmentStatePagerAdapter
是為了懶加載及頁(yè)面回收的目的而編寫(xiě)的, 即不把每個(gè) page 頁(yè)的內(nèi)容都保存在內(nèi)存里. 因此它在創(chuàng)建了 Fragment 后, 沒(méi)有給其附加 tag. 所以由它創(chuàng)建的 Fragment 無(wú)法用FragmentManager.findFragmentByTag()
方法找到. 具體見(jiàn)其源碼, 也不長(zhǎng).
方法二
還有一種思路, 是重載 FragmentPagerAdapter 類(lèi)中的 instantiateItem()
方法, 得到 Fragment 引用. 依然以 示例代碼1 為例, 將 SlidePagerAdapter 做如下改寫(xiě)即可:
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秒改變文字. 如果立刻執(zhí)行, 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;
}
}
}
因?yàn)?instantiateItem()
方法管理了 Fragment 的創(chuàng)建及重用, 因此無(wú)論其是新創(chuàng)建的, 還是被恢復(fù)的, 都可以正確取到引用.
注意: 不要在
FragmentStatePagerAdapter
場(chǎng)景中使用該方法. 因?yàn)槲覀儽4媪嗣恳豁?yè)的 Fragment 的引用, 就會(huì)阻止其被回收, 那 FragmentStatePagerAdapter 就白用了: 不就是為了可以回收頁(yè)面才用它的嘛.
真要用的話(huà)就用WeakReference<Fragment>
保存其弱引用.
但據(jù)說(shuō) 4.0 后的 Android 虛擬機(jī)中弱引用等于沒(méi)引用, 會(huì)很快被回收掉. (這句是聽(tīng)一位虛擬機(jī)大牛說(shuō)的)