Fragment and PagerAdapter
前情提要
最近的项目中,又用到了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被回收,比如横竖屏切换,或者内存紧张后台应用程序回收。
这里存在两个问题。
- 此时导致了
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()
}
- 系统在恢复
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)来传递参数