前情提要
最近的项目中,又用到了Fragment+FragmentPagerAdapter
的组合。
不禁想起当年第一次使用这两者结合的一些窘境。
平常开发使用时,经常别人选定了框架,你负责开枝散叶,而这开枝散叶的第一步经常是Crlt+C 和Crlt+V。把别人写好的FragmentOne
复制一份FragmentSecond
,然后把View的内容改一改。模仿其初始化,加入 PagerAdapter中。
等到自己第一次从头写FragmentOne
的时候,就有点手足无措了。
至于使用Fragment+PagerAdapter
,而不是View+PagerAdapter
,我一直以来都只有一个原因,对于复杂的布局,那就是Fragment有相对独立的生命周期,一切有迹可循,将代码从Activity中抽离,简化Activity的逻辑。
何况JetPack框架中 ViewModel
对于Fragment的支持。
本篇涉及两个点。
Fragment的系统回收以及初始化
-
FragmentPagerAdapter
和FragmentStatePagerAdapter
的区别
下一篇则偏PagerAdapter的基础分析以及实战:
Fragment 与 FragmentPagerAdapter (二)
先贴上Fragment 生命周期图:
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被回收,比如横竖屏切换,或者内存紧张后台应用程序回收。
就要注意两个问题!
- 此时
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()
}
- 系统在恢复
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系统回收,多说两句:
- 可能你注意到了,上面贴的代码中,两个PagerAdapter都有对Fragment的复用进行了处理,但二者之间获取恢复的Fragment的手段并不一致,
FragmentPagerAdapter直接通过tag,从FragmentManager中的所有Fragment中寻找。
而FragmentStatePagerAdapter,在ViewPager.setAdapter()时,从ViewPager的saveState中的获取原先所有处于active的Fragment的key值(被回收时保存进去的),然后再从FragmentManger中的activeMap中获取Fragment。
- 当Activity恢复时,Fragment中的子Fragment也是会恢复的。也就是
childFragmentManager
中也会有Fragment的。
所以 若有自己复写PagerAdapter的,记得重写instantiateItem()时,要判断下,FragmentManger或childFragmentManager
中是否有缓存,避免内存泄露或其他意外事件。
- 上面那个问题,也就是下面这段代码真正的问题出在哪?又应该如何改善,留给读者思考了:
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)来传递参数