Android 保存 Fragment 引用及 getActivity() 为空问题

问题

作 Android 应用开发的小伙伴们大多都被 Fragment 坑过. 最近研究了其中常见的一种坑, 记录下来, 以避免遗忘. 问题大致是这样的:
有时咱们但愿在 Activity 中保存所建立的 Fragment 的引用, 以便后续逻辑中作界面更新等操做. 若是页面中的 Fragment 都是静态的 (不会被 remove, hide 等), 则通常不会出啥问题. 若是是多个 Fragment 切换的场景, 就容易出现 getActivity() 为 null 等问题. 这种问题在使用 FragmentPagerAdapter 时尤为容易出现.
这里涉及两个问题: Fragment 的建立和 Fragment 引用的保存. 两个问题都有坑.java

先放结论 (编程建议):android

  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() 来建立它.app

回到 示例代码1, 在正常状况下, 这段代码是能够完美运行的. 但若是咱们的界面被系统回收掉了, 当用户再次返回这个界面时, 问题就来了. 在这种状况下:ide

  • 由于 Activity 被销毁了, 所以 onCreate() 会被调用, 咱们的三个 Fragment 会被从新建立并装入 mFragmentList 数组.
  • 又由于 Activity 被销毁了, 所以系统会自动恢复界面状态, 包括以前已经被添加的 Fragment. 恢复完成后, 轮到 FragmentPagerAdapter 显示 fragment1. FragmentPagerAdapter 经过 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 虚拟机中弱引用等于没引用, 会很快被回收掉. (这句是听一位虚拟机大牛说的)
相关文章
相关标签/搜索