平常大家都能聽到,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,并且數量比較多時適用