平常大家都能听到,ViewPager默认缓存三个子项,FragmentPagerAdapter会保存所有的Fragment,子项多时应该尽量不要用它,那么究竟它是怎么保存所有子项的呢?
为了测试三者缓存策略(创建与销毁子view)的区别,在ViewPager三种Adapter的子view创建和销毁的方法添加相关的日志代码,如下:
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
Log.d("ccc", "destroyItem:" + position);
//...省略部分代码
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
Log.d("ccc", "instantiateItem:" + position);
//...省略部分代码
}
滑动ViewPager翻页,观察控制台的输出,三种Adapter针对不同界面、不同滑动方向的翻页情况打印如下:
从图中我们可以看到,三种Adapter在相同的情况下,ViewPager的子页面销毁和创建时机是一样。但是这好像违背了我们之前学过的,因为我们通常所听到的都是FragmentPagerAdapter会缓存所有的Fragment子项,而上图中我们看到的是在滑动的过程中它的destroyItem方法被调用了,而在滑动回来时相对应的子项Fragment也确实调用instantiateItem方法。根本就没有缓存?!
但是仔细对比了一下三个Adapter创建视图的过程,发现自己错了,因为在使用Fragment作为子视图时,我们是通过getItem方法返回Fragment的,单纯从这里打印instantiateItem的调用不代表Fragment真的完全被重新创建了(重新创建代表需要重新add,即从头走一遍生命周期,但是在这里不能证明),也可以通过两个FragmentAdapter中instantiateItem的实现证明(观察getItem方法的调用条件),所以又在Fragment对应的两种Adapter的getItem中添加相应的log代码,如下:
@Override
public Fragment getItem(int position) {
Log.d("ccc", "getItem:" + position);
return fragmentList.get(position);
}
针对不同情况,控制台输出结果如下:
通过上图我们可以看到,FragmentPagerAdapter在最后向右边划回来时并没有调用getItem方法(getItem是创建一个新的Fragment),这也就说明了他没有重新创建Fragment,证明了它会缓存所有Fragment,那么它到底在哪里做了缓存呢?
百思不得其解,只好去看源码了,如下:
FragmentPagerAdapter#destroyItem:
@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);
}
以上,我们关注方法的最后一行代码,在这里,ViewPager的Fragment子项并没有真正的被移除(FragmentTransaction没有调用remove方法),FragmentTransaction只是调用了detach方法。detach和remove不同,detach后Fragment的状态依然保持着,在使用attach()时会再次调用onCreateView()来重绘视图。而在FragmentStatePagerAdapter的destroyItem方法中是直接调用了remove方法。
这里说明一下detach和remove的区别(结合Fragment的声明周期说明)
detach:
对应执行的是Fragment生命周期中onPause()-onDestroyView()的方法,此时并没有执行onDestroy和onDetach方法。所以在恢复时只需要attach方法即可(可以在FragmentPagerAdapter的instantiateItem方法中看到调用,对应源码下面给出),attach方法对应的是执行Fragment生命周期中onCreateView()-onResume()。
@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 = mFragmentManager.findFragmentByTag(name);
if (fragment != null) {
if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
mCurTransaction.attach(fragment);
} else {
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;
}
remove:
比起detach,remove的做法很直接,它直接对应执行了Fragment生命周期中onPause()-onDetach()的方法,所以在恢复(这里需要重新add了)时,需要执行的是add方法(可以在FragmentPagerStateAdapter的instantiateItem方法中看到调用,对应源码下面给出),add方法对应执行了Fragment生命周期中onAttach()-onResume()的方法。
@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);
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;
}
- 从第一张三种Adapter滑动时打印的日志图中,我们也了解到:除了滑动到第一个和最后一个子界面,ViewPager始终是缓存三个子界面(这里的缓存指的是ViewPager同一时间会加载三个子view)!
- 而三种Adapter的缓存策略则各有不同:
- PagerAdapter:缓存三个,通过重写instantiateItem和destroyItem达到创建和销毁view的目的。
- FragmentPagerAdapter:内部通过FragmentManager来持久化每一个Fragment,在destroyItem方法调用时只是detach对应的Fragment,并没有真正移除!
- FragmentPagerStateAdapter:内部通过FragmentManager来管理每一个Fragment,在destroyItem方法 调用时移除对应的Fragment。
- 所以,我们分情况使用这三个Adapter
PagerAdapter:当所要展示的视图比较简单时适用
FragmentPagerAdapter:当所要展示的视图是Fragment,并且数量比较少时适用
FragmentStatePagerAdapter:当所要展示的视图是Fragment,并且数量比较多时适用