问题
做 Android 应用开发的小伙伴们大多都被 Fragment 坑过. 最近研究了其中常见的一种坑, 记录下来, 以免遗忘. 问题大体是这样的:
有时我们希望在 Activity 中保存所创建的 Fragment 的引用, 以便后续逻辑中做界面更新等操作. 如果页面中的 Fragment 都是静态的 (不会被 remove, hide 等), 则一般不会出啥问题. 如果是多个 Fragment 切换的场景, 就容易出现 getActivity() 为 null 等问题. 这种问题在使用 FragmentPagerAdapter 时尤其容易出现.
这里涉及两个问题: Fragment 的创建和 Fragment 引用的保存. 两个问题都有坑.
先放结论 (编程建议):
- 不要在 Activity.onCreate() 中直接
new Fragment()
. Fragment 的创建应尽量纳入 FragmentManager 的管理. - 尽量不要保存 Fragment 的引用. 在需要直接调用 Fragment 时, 使用 FragmentManager.findFragmentByTag() 等方法获取相关 Fragment 的引用.
- 如果一定要保存 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()
来创建它.
回到 示例代码1, 在正常情况下, 这段代码是可以完美运行的. 但如果我们的界面被系统回收掉了, 当用户再次返回这个界面时, 问题就来了. 在这种情况下:
- 因为 Activity 被销毁了, 因此 onCreate() 会被调用, 我们的三个 Fragment 会被重新创建并装入 mFragmentList 数组.
- 又因为 Activity 被销毁了, 因此系统会自动恢复界面状态, 包括之前已经被添加的 Fragment. 恢复完成后, 轮到 FragmentPagerAdapter 显示 fragment1. FragmentPagerAdapter 通过
FragmentManager.findFragmentByTag()
, 发现 fragment1 已经被添加了 (被添加的为老 Fragment, 即被系统恢复的那个). 因此不会再去调用FragmentPagerAdapter.getItem()
, 因此 FragmentPagerAdapter 直接显示了被系统恢复出来的 fragment1.
没错, 这种情况下, Fragment1 在 Activity 中其实有两个实例:
一个是真正的被 Activity 添加并显示的实例;
一个是在 onCreate() 中被创建, 并保存在 mFragmentList 中的没有什么卵用的实例.
可以想见, 这种状态下肯定会出现很多莫名其妙的问题, 其中就包括 getActivity()
返回 null 的问题.
吐槽:
FragmentPagerAdapter.getItem()
方法明明就是 FragmentPagerAdapter 用来内部创建 Fragment 用的啊, 根本不是用来供外部获取 Fragment 用的. 如果改名叫createItem()
或者createFragment()
之类的, 估计可以防止不少人掉坑的.
代码修正
基于以上分析可知, 在 Activity.onCreate()
中创建 Fragment 是不恰当的. 应该把 Fragment 的创建放在 FragmentPagerAdapter.getItem()
中. 经过改进的 示例代码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 保存各个 Fragment 的引用了, Fragment 的创建完全交给 FragmentPagerAdapter 去做.
其实在其他的使用 Fragment 的场景中, 也会出现上述问题, 也应该遵循同样的原则, 即文章开头所列的 建议1 和 建议2 .
这样是解决了上面提到的 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 虚拟机中弱引用等于没引用, 会很快被回收掉. (这句是听一位虚拟机大牛说的)