Fragment 与 FragmentPagerAdapter (一)

前情提要

最近的项目中,又用到了Fragment+FragmentPagerAdapter的组合。
不禁想起当年第一次使用这两者结合的一些窘境。

平常开发使用时,经常别人选定了框架,你负责开枝散叶,而这开枝散叶的第一步经常是Crlt+C 和Crlt+V。把别人写好的FragmentOne复制一份FragmentSecond,然后把View的内容改一改。模仿其初始化,加入 PagerAdapter中。
等到自己第一次从头写FragmentOne的时候,就有点手足无措了。

至于使用Fragment+PagerAdapter,而不是View+PagerAdapter,我一直以来都只有一个原因,对于复杂的布局,那就是Fragment有相对独立的生命周期,一切有迹可循,将代码从Activity中抽离,简化Activity的逻辑。
何况JetPack框架中 ViewModel对于Fragment的支持。

本篇涉及两个点。

  • Fragment的系统回收以及初始化
  • FragmentPagerAdapterFragmentStatePagerAdapter的区别

下一篇则偏PagerAdapter的基础分析以及实战:
Fragment 与 FragmentPagerAdapter (二)

先贴上Fragment 生命周期图:


image.png

1.FragmentPagerAdapter 和 FragmentStatePagerAdapter的区别

再说这两的区别

FragmentPagerAdapter

基本是很多博客举栗子的时候都喜欢用这个,Fragment对象都是创建好放在List中,

val fragmentList = mutableListOf(
    FragmentOne(),
    FragmentTwo(),
    FragmentThree()
)

    val adapter = object : FragmentPagerAdapter(childFragmentManager) {
        override fun getItem(position: Int): Fragment {
            return fragmentList[position]
        }
        override fun getCount(): Int {
            return fragmentList.size
        }

    }

当初年少无知的我,一脸懵逼,为什么总是把Fragment先创建好,这不是浪费内存,和影响回收么。之所以这么想,是因为对该机制还不是很了解啊。

当然这样子写,在内存不足时,该ViewPager所在的Activity被回收的情况下,还是依然会存在问题的。看完文章的最后一节,大家应该也应该可以很容易明白问题出在哪里了。

要知道,FragmentPagerAdapter本身就是用于少量静态页面的处理。

当在不同的位置来回切换,其实adapter.getItem()只会被调用一次用于初始化Fragment。当划出缓存范围时,这个Fragment实例依旧保存在FragmentManager中,并不会被销毁。仅是执行了mCurTransaction.detach(fragment),根据代码注释,也就是类似于加入回退栈---也就是并未Fragment.onDestory(),只销毁了视图。

    @Override
    public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
        if (DEBUG) Log.v(TAG, "Detaching item #" + getItemId(position) + ": f=" + object
                + " v=" + ((Fragment)object).getView());
        mCurTransaction.detach((Fragment)object); // 将Fragment从视图中移除,而保留实例
    }

再次滑动到该页面时,fragment执行生命周期onCreateView()重新创建UI视图(但不会执行onCreate())。

接着我再来康康ViewPager每个Item的初始化,可以对照下FragmentPagerAdapter的instantiateItem()源码的注释:

FragmentPagerAdapter:

public Object instantiateItem(@NonNull ViewGroup container, int position) {
        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }

        final long itemId = getItemId(position);

        // Do we already have this fragment?
        //根据container.getId(), itemId生成TAG, itemId即position
        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 {
            //如果找不到实例,才调用getItem()
            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;
    }

所以即便是动态创建Fragment,也就是起一个延迟初始化的作用。那么fragmentList直接把所有Fragment对象创建好并没有很大影响。除非说,Fragment初始化之时,就保存了大量的数据。否则毕竟只是几个对象而已,内存占用很小。

等到getItem()被调用时,创建好的Fragment才会开始拥有其生命周期,保存有UI视图,真正的占有大量的内存。

当然,如果有需要话,简单的处理,可以在getItem的时候再去初始化。比如说

override fun getItem(position: Int): Fragment {
   return when(position) {
       0 -> FragmentOne()
       1 -> FragmentTwo()
       2 -> FragmentThree()
       else -> Fragment()
   }

既然实例没有被销毁,如果出于某些考虑,比如更快的显示view视图,甚至可以在Fragment实例中用变量缓存原本要被销毁的View,然后在onCreateView中复用。当然这样会占用更多内存。

另外由于视图被销毁,但是实例存在,那么需要考虑好实例的变量的值对新创建的View的影响。

FragmentStatePagerAdapter

FragmentPagerAdapter不同的是,不在ViewPager缓存范围的Fragment实例会从FragmentManager中移走,只保留其状态(各种Bundle参数,包括view状态等),当再次加载该位置时,保留的状态会恢复。

public Object instantiateItem(@NonNull ViewGroup container, int position) {
        // If we already have this item instantiated, there is nothing
        // to do.  This can happen when we are restoring the entire pager
        // from its saved state, where the fragment manager has already
        // taken care of restoring the fragments we previously had instantiated.
        if (mFragments.size() > position) {
            Fragment f = mFragments.get(position);
            if (f != null) { // 不仅缓存,若系统回收 恢复时也是从这里读取
                return f;
            }
        }

        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }

        Fragment fragment = getItem(position); // 与FragmentPagerAdapter不同,直接getItem()
        if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
        if (mSavedState.size() > position) { // 获取保存的数据
            Fragment.SavedState fss = mSavedState.get(position);
            if (fss != null) {
                fragment.setInitialSavedState(fss);
            }
        }
        while (mFragments.size() <= position) {
            mFragments.add(null);
        }
        fragment.setMenuVisibility(false);
        fragment.setUserVisibleHint(false);
        mFragments.set(position, fragment);
        mCurTransaction.add(container.getId(), fragment);

        return fragment;
    }
    @Override
    public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
        Fragment fragment = (Fragment) object;

        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
        if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object
                + " v=" + ((Fragment)object).getView());
        while (mSavedState.size() <= position) {
            mSavedState.add(null);
        }
        //保留Fragment的状态
        mSavedState.set(position, fragment.isAdded()
                ? mFragmentManager.saveFragmentInstanceState(fragment) : null);
        mFragments.set(position, null);
        // 从FragmentManager中移除
        mCurTransaction.remove(fragment);
    }

FragmentStatePagerAdapter中,ViewPager不同位置的Fragment会都被同时保存在一个ArrayList中的,超出缓存位置,执行destroyItem(),ArrayList相应位置也会将其置空,也就是不保留Fragment实例。

但保留有Fragment的状态:

mSavedState.set(position, fragment.isAdded()
                ? mFragmentManager.saveFragmentInstanceState(fragment) : null);
mFragments.set(position, null);

所以这里踩的一个小坑是:

曾想做一个无限滑动的ViewPager,使得getCount()返回的是Int.MAX_VALUE,而且用的是FragmentStatePagerAdapter,那么在初始化 Fragment会调用mFragments.set(position, fragment);position比较大时直接在ArrayList扩容时直接OOM了。

所以,综上,如果对于少量的静态页面直接使用FragmentPagerAdapter
而如果有大量的动态页面还是使用FragmentStatePagerAdapter,毕竟无需保留所有Fragment的实例。

2. Fragment的系统回收以及初始化

Fragment的初始化,不涉及生命周期的话,其实没多少可以说的,毕竟,不就是一个对象嘛,直接Fragment()创建轻轻松松,或者继承时,构造函数加个参数也没什么大不了的,

 FragmentOne("param")..so easy

当然代码中常见还有这这种,比如上述FragmentPagerAdapter动态初始化Fragment

val map = arrayOf(
        TodayFragment::class.java,
        LastDayFragment::class.java)

fun getFragment(position: Int) : Fragment{
        return Fragment.instantiate(this.context, map[position].name)
}

这里主要要讲的就是Fragment.instantiate()

public static Fragment instantiate(Context context, String fname, @Nullable Bundle args) {
        try {
            Class clazz = sClassMap.get(fname);
            if (clazz == null) {
                // Class not found in the cache, see if it's real, and try to add it
                clazz = context.getClassLoader().loadClass(fname);
                sClassMap.put(fname, clazz);
            }
            Fragment f = (Fragment) clazz.getConstructor().newInstance();
            if (args != null) {
                args.setClassLoader(f.getClass().getClassLoader());
                f.setArguments(args);
            }
            return f;
}

这里可以看到其实该方法就是直接调用的Fragment的默认构造方法,并执行setArguments(args)来设置参数。

看到这里,有人会不禁的想,既然是调用默认构造方法,我直接使用Fragment()或者在有参数的情况下,直接FragmentOne("param")不是来得更容易?

ok,当然更容易啦。

不过我们要考虑一种情况就是,Activity在非用户主动退出的情况下,Activity被回收,比如横竖屏切换,或者内存紧张后台应用程序回收。

就要注意两个问题

  1. 此时Fragment也被回收,同时系统新建Activity时也会恢复被回收的Fragment。所以需要可能需要判断防止多生成一个Fragment
xxxxActivity extend FragmentActivity:

override fun onCreate(savedInstanceState: Bundle?) {
    //看里面的源码,在onCreate()会恢复Fragment
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    savedInstanceState ?: let { // 这里怎么复用就见招拆招了
        // 直接新建一个替换相同id的Fragment,不复用原数据
        supportFragmentManager.beginTransaction().replace(R.id.main,FragmentOne("param")).commit()
    }
    initView()
}
  1. 系统在恢复Fragment的时候,调用的是Fragment.instantiate()了来创建对象,也就表示Fragment必须有无参构造方法。
class FragmentOne : Fragment {

    constructor() : super() {} // /必须有无参构造方法
    constructor(p: String) : this() {
        
}

也就是回收恢复时创建FragmentOne对象调用的是FragmentOne(),而不是FragmentOne("param"),那么"param" 参数没有被传进去就可能导致一些错误。这时候,也就是setArguments(args)来起作用了.

Fragment被回收时,会保存Fragment状态---FragmentState,意味着Fragment中通过setArguments(args)方法之后的args对象也会被保存下来!那么就恢复的fragment实例就可以通过getArguments()来获取到该值了

所以在我们在创建 FragmentOne("param")传递参数时记得调用setArguments(args),把param保存下来。

也可以写成这样(Android Studio 模板代码):

class BlankFragment: Fragment() {

    private var param1: String? = null
    private var param2: String? = null
     override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments?.let {
            param1 = it.getString(ARG_PARAM1)
            param2 = it.getString(ARG_PARAM2)
        }

    companion object {
        @JvmStatic
        fun newInstance(param1: String, param2: String) =
            BlankFragment().apply {
                arguments = Bundle().apply {
                    putString(ARG_PARAM1, param1)
                    putString(ARG_PARAM2, param2)
                }
            }
    }

关于Fragment系统回收,多说两句:

  1. 可能你注意到了,上面贴的代码中,两个PagerAdapter都有对Fragment的复用进行了处理,但二者之间获取恢复的Fragment的手段并不一致,

FragmentPagerAdapter直接通过tag,从FragmentManager中的所有Fragment中寻找。

而FragmentStatePagerAdapter,在ViewPager.setAdapter()时,从ViewPager的saveState中的获取原先所有处于active的Fragment的key值(被回收时保存进去的),然后再从FragmentManger中的activeMap中获取Fragment。

  1. 当Activity恢复时,Fragment中的子Fragment也是会恢复的。也就是childFragmentManager中也会有Fragment的。

所以 若有自己复写PagerAdapter的,记得重写instantiateItem()时,要判断下,FragmentManger或childFragmentManager中是否有缓存,避免内存泄露或其他意外事件。

  1. 上面那个问题,也就是下面这段代码真正的问题出在哪?又应该如何改善,留给读者思考了:
val fragmentList = mutableListOf(
    FragmentOne(),
    FragmentTwo(),
    FragmentThree()
)

    val adapter = object : FragmentPagerAdapter(childFragmentManager) {
        override fun getItem(position: Int): Fragment {
            return fragmentList[position]
        }
        override fun getCount(): Int {
            return fragmentList.size
        }

    }

参看问答

参考:
Android解惑 - 为什么要用Fragment.setArguments(Bundle bundle)来传递参数

你可能感兴趣的:(Fragment 与 FragmentPagerAdapter (一))