作 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 虚拟机中弱引用等于没引用, 会很快被回收掉. (这句是听一位虚拟机大牛说的)