Fragment and PagerAdapter

作者: 真心czx | 来源:发表于2019-07-23 21:22 被阅读13次

前情提要

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

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

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

本文涉及两个点。

  • Fragment的初始化
  • FragmentPagerAdapter 和 FragmentStatePagerAdapter的区别

1.FragmentPagerAdapter 和 FragmentStatePagerAdapter的区别

先说这两的区别

FragmentPagerAdapter

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

val fragmentList = mutableListOf<Fragment>(
    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先创建好,这不是浪费内存,和影响回收么。

之所以这么想,是因为对该机制还不是很了解啊

要知道,FragmentPagerAdapter本身就是用于少量静态页面的处理。
不同的position,adapter.getItem()只会被调用一次,并且这个item实例会被保存在FragmentManager中,并不会被销毁。仅是执行了mCurTransaction.detach(),根据代码注释,也就是类似于加入回退栈,从界面上不显示了,再次回到该页面执行显示时,fragment执行生命周期onCreateView()重新创建UI视图(不会执行onCreate())。

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;
    }

    @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,也就是起一个延迟初始化的作用。那么fragmentList直接把所有Fragment对象创建好并没有很大影响,除非说,Fragment初始化之时,就保存了大量的数据。否则毕竟只是几个对象而已,内存占用很小。只有等到调用该位置的时候,创建好的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);
        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);

        mCurTransaction.remove(fragment);
    }

可以看到,FragmentStatePagerAdapter中,Fragment是存在一个ArrayList中,超出缓存位置,就会执行destroyItem(),而移出ArrayList,不保留Fragment实例,但保留有Fragment的状态。

所以这里踩的一个小坑是,曾想做一个无限滑动的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。所以需要在 onCreate()判断,防止多生成一个Fragment
xxxxActivity extend FragmentActivity:

override fun onCreate(savedInstanceState: Bundle?) {
    //看里面的源码,在onCreate()会恢复Fragment
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    savedInstanceState ?: let {
        supportFragmentManager.beginTransaction().replace(R.id.main,FragmentOne("param")).commit()
    }
    initView()
}
  1. 系统在恢复Fragment的时候,调用的却是Fragment.instantiate()了,也就是创建FragmentOne对象调用的是FragmentOne(),而不是FragmentOne("param"),那么"param" 参数没有被传进去就可能导致一些错误。
    这时候,也就是setArguments(args)来起作用了.
    Fragment被回收时,会保存Fragment状态---FragmentState,也就是Fragment中通过setArguments(args)方法之后的mArgument变量也会被保存下来!那么就恢复的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)
                }
            }
    }

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

相关文章

网友评论

    本文标题:Fragment and PagerAdapter

    本文链接:https://www.haomeiwen.com/subject/uxbhlctx.html