最近碰到个比较奇葩的需求,总结问题就是ViewPager和Fragment搭配使用(3种以上类型的Fragment),Fragment列表中的不同种Fragment顺序改变,或者删除增加导致的各种崩溃问题,不论使用FragmentPagerAdapter还是FragmentStatePagerAdapter都会出现不同类型的错误,也让我看到了这两种PageAdapter的局限性,在此总结,希望能帮到遇到同样问题的亲们。
本篇内容
一、FragmentPagerAdapter和FragmentStatePagerAdapter的区别以及局限性
二、由于ViewPager中Fragment列表的增删和顺序变化,导致ViewPager调用notifyDataSetChanged()方法出现异常或应用崩溃的解决方案
————————————————————————————————
内容一:FragmentPagerAdapter和FragmentStatePagerAdapter的区别以及局限性
FragmentPagerAdapter和FragmentStatePagerAdapter,这两个adapter刚开始用的时候分不清,不过似乎不论用哪个adapter出来的效果都差不多,用的多的人知道,当viewpager中fragment数量多的时候用FragmentStatePagerAdapter,反之则用FragmentPagerAdapter。在这里我想从两个问题展开,看清这两个问题,大家就一目了然了:
1.两种adapter存储/恢复fragment的区别
2.两种adapter销毁fragment的区别
存储和恢复:
FragmentPagerAdapter
初始化方法:
@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 fragment = mFragmentManager.findFragmentByTag(name);//从manager中找出该名字的fragment
if (fragment != null) {
if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
mCurTransaction.attach(fragment);
} else {
/*
继承FragmentPagerAdapter要实现的getItem方法,从源码中可以看到,
不是viewpager跳转到哪个fragment就会调用getItem方法,
而是只要manager中没有这个fragment才会调用,
一个fragment只会调用一次这个方法
*/
fragment = getItem(position);
if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
mCurTransaction.add(container.getId(), fragment,
makeFragmentName(container.getId(), itemId));//将命名的fragment放入manager进行管理
}
if (fragment != mCurrentPrimaryItem) {
fragment.setMenuVisibility(false);
fragment.setUserVisibleHint(false);
}
return fragment;
}
以上代码要注意两点,一个是getItem方法的调用时机,只有当manager中没有该Fragment的时候才会被调用,一个Fragment只会触发这一次,原因就是要注意的第二点,方法makeFragmentName(),通过父容器container的Id和getItemId()方法返回的值(不重写该方法就返回fragment的position值),为每一个fragment命名,然后通过键值对的方式,放入到manager中,从而达到存储fragment和恢复fragment的目的。然而这个makeFragmentName()也就是当fragment列表顺序改变(即position值改变)导致崩溃的原因。
因此这个FragmentPagerAdapter不适用于fragment列表顺序改变,中间添加fragment或者删除某个fragment的情形。
销毁方法:
@Override
public void destroyItem(ViewGroup container, int position, 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);//仅detach该fragment 没做任何释放内存操作
}
这是销毁fragment的方法,可以看到FragmentPagerAdapter不是将不可见的fragment销毁,而是仅仅将该fragment从页面中detach掉,fragment还是在manager中保存,内存没有被释放,从这边可以看到FragmentPagerAdapter不适合fragment数量多的情况下使用,因为未被释放的fragment会占用大量内存。
FragmentStatePagerAdapter
初始化方法
/*
FragmentStatePagerAdapter是通过mFragments数组来存储fragment的,通过mSavedState列表
来存储fragment销毁时的状态,通过position获取到的fragment可能为空(被回收),如果为空,则会
再次调用getItem方法重新创建新的fragment,然后将mSavedState中存储的状态重新赋予这个新的fragment,
达到fragment恢复的效果
*/
@Override
public Object instantiateItem(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);//创建fragment 因为fragment频繁的创建导致这个getItem方法会被多次调用 区别于FragmentPagerAdapter
if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
if (mSavedState.size() > position) {
Fragment.SavedState fss = mSavedState.get(position);//从存储fragment的列表中取出对应位置fragment保存的状态
if (fss != null) {
fragment.setInitialSavedState(fss);//将之前fragment状态赋予新的fragment
}
}
while (mFragments.size() <= position) {
mFragments.add(null);
}
fragment.setMenuVisibility(false);
fragment.setUserVisibleHint(false);
mFragments.set(position, fragment);
mCurTransaction.add(container.getId(), fragment);
return fragment;
}
以上代码的中mFragments和mSavedState是按顺序一一对应的,每当一个fragment重建都会从mSavedState中找到相应的状态,如果mFragment中的某一个fragment顺序改变,那么mFragments和mSavedState就不再一一对应,导致fragment恢复时出现异常情况。
同样这个FragmentStatePagerAdapter不适用于fragment列表顺序改变,中间添加fragment或者删除某个fragment的情形。
销毁方法:
Override
public void destroyItem(ViewGroup container, int position, 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);
}
mSavedState.set(position, fragment.isAdded()
? mFragmentManager.saveFragmentInstanceState(fragment) : null);//释放引用之前保存该位置fragment的状态
mFragments.set(position, null);//将mFragment列表中该位置的fragment至空,释放引用
mCurTransaction.remove(fragment);//同时将manager中的该fragment释放
}
FragmentPagerAdapter销毁fragment方法,可以看到,当fragment在页面中不可见时,该fragment的状态会先被保存到mSavedState中,而fragment实例则会被销毁,在对应的instantiateItem方法中,fragment会被重新创建,并将mSavedState中对应状态赋予该刚刚创建的新fragment,从而达到恢复之前fragment和节省内存的效果,因此FragmentStatePagerAdapter适合有较多fragment情况
————————————————————————————————
内容二,由于ViewPager中Fragment列表的增删和顺序变化,导致ViewPager调用notifyDataSetChanged()方法出现异常或应用崩溃的解决方案.
看完以上源码,相信大家应该明白,发生fragment列表顺序改变,中间添加fragment或者删除某个fragment这些情况的时候,为什么会出现异常等情况。我们需要根据需求,定制这两种不同的adapter,所以在此给出发生这些异常时的解决思路:
1、若Fragment数量较少,可采用FragmentPagerAdapter的方式,要做的是定制makeFragmentName()该方法,让instantiateItem获取fragment时不根据position和itemid值,而是根据你的需求对fragment进行命名,同时当fragment列表数量减少时,从manager中删除被删除的fragment引用,让其内存得到释放。
2、若Fragment数量较多,可采用FragmentStatePagerAdapter的方式,要做的是改变其中mFragments和mSavedState的对应关系,不再根据position进行一一对应。
(我说的方式当然是把两个adapter的源码复制一遍然后修改啦!)
最后请注意重写adapter中的这个方法
@Override
public int getItemPosition(Object object) {
//object参数是当前位置的fragment
//return POSITION_UNCHANGED;
//return POSITION_NONE;
}
果返回POSITION_UNCHANGED,那么该位置的fragment是不会被更新的,POSITION_NONE才会重新更新这个位置fragment
例子:如果要更新的fragment列表中第一个fragment是固定不变的,那么这个位置的fragment可以返回POSITION_UNCHANGED,而其他位置改变的fragment则要返回POSITION_NONE。
---------------------
当
viewpager+tablayout+fragment切换时嵌套复用的时候,fragment与对应显示内容不符时,使用FragmentStatePagerAdapter就可完美解决。。。。。。。。。