Android 保存 Fragment 引用及 getActivity() 为空问题
来源:互联网 发布:网络教育龙头股 编辑:程序博客网 时间:2024/06/06 04:00
问题
做 Android 应用开发的小伙伴们大多都被 Fragment 坑过. 最近研究了其中常见的一种坑, 记录下来, 以免遗忘. 问题大体是这样的:
有时我们希望在 Activity 中保存所创建的 Fragment 的引用, 以便后续逻辑中做界面更新等操作. 如果页面中的 Fragment 都是静态的 (不会被 remove, hide 等), 则一般不会出啥问题. 如果是多个 Fragment 切换的场景, 就容易出现 getActivity() 为 null 等问题. 这种问题在使用 FragmentPagerAdapter 时尤其容易出现.
这里涉及两个问题: Fragment 的创建和 Fragment 引用的保存. 两个问题都有坑.
先放结论 (编程建议):
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()
来创建它.
回到 示例代码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 虚拟机中弱引用等于没引用, 会很快被回收掉. (没深入研究过 JVM, 这句是听一位虚拟机大牛说的)
- Android 保存 Fragment 引用及 getActivity() 为空问题
- fragment getActivity()为空
- fragment使用getActivity(),出现getActivity()为空
- fragment中getactivity为空
- 解决在Fragment中getActivity()为空问题
- 在Fragment中getActivity()为空问题已解决
- Fragment中getActivity()和getContext()为空的问题
- fragment遇到getactivity为空的情况
- 解决Android里getActivity()为空的问题
- fragment里getactivity空指针问题
- Android---Fragment中getActivity()提示空指针
- fragment getActivity()空指针
- Fragment getActivity()空指针?
- Fragment中getActivity为null的问题
- Activity被回收导致fragment的getActivity为空
- fragment的handler中getActivity空指针问题
- Android填坑之旅(第二篇) 关于Fragment中getActivity为Null的问题
- Android解决多个Fragment切换时布局重新实例化问题和getActivity空指针问题
- 更新yum支持高版本Lamp
- IO流各个类和对象的小结
- android中radioGroup动态添加radioButton
- Java hashcode
- ArduCopter相关
- Android 保存 Fragment 引用及 getActivity() 为空问题
- Android应用开发所需技能自我评测
- EasyUI中combobox的使用
- Codevs 1482 路线统计(矩阵乘法)
- 优雅的点
- 一起学爬虫 Node.js 爬虫篇(一)
- 并行消费kafka存放本地文件
- 使用C#的HttpWebRequest模拟登陆网站
- Elasticsearch中X-Pack破解试用