ViewPager重新整理問題原理分析及解決方案(FragmentPagerAdapter+FragementStatePagerAdapter)

看書的小蝸牛發表於2018-01-03

Android開發中經常用到ViewPager+Fragment+Adapter的場景,一般每個Fragment控制自己的重新整理,但是如果想要重新整理整個ViewPager怎麼做呢?或者想要將快取的Fragent給重建怎麼做呢?之前做業務的時候遇到一個問題,ViewPage在第二次setAdapter的如果用的是FragmentPager並不會導致頁面重新整理,但是採用FragementStatePagerAdapter卻會重新整理?不由得有些好奇,隨跟蹤了部分原始碼,簡單整理如下:

ViewPager+FragmentPagerAdapter為何不能通過setAdapter做到整體重新整理

第二次設定PagerAdapter的時候,首先會將原來的Fragment進行清理,之後在呼叫populate()重建,只是重建的時候並不一定真的重新建立Fragment,如下:

public void setAdapter(PagerAdapter adapter) {
    if (mAdapter != null) {
        ...
        for (int i = 0; i < mItems.size(); i++) {
            final ItemInfo ii = mItems.get(i);
            <!--全部destroy-->
            mAdapter.destroyItem(this, ii.position, ii.object);
        }
        mAdapter.finishUpdate(this);
        <!--清理-->
        mItems.clear();
        removeNonDecorViews();
        <!--重置位置-->
        mCurItem = 0;
        scrollTo(0, 0);
    }
    ...
    if (!wasFirstLayout) {
    <!--重新設定Fragment-->
       populate();
       }
    ...   
    }        
複製程式碼

之前說過,第二次通過setAdapter的方式來設定ViewPager的FragmentAdapter時不會立即重新整理的效果,但是如果往後滑動幾屏會發現其實是有效果了?為什麼呢,因為第二次setAdapter的時候,已經被FragmentManager快取的Fragent不會被新建,也不會被重新整理,因為FragmentAdapter在呼叫destroy的時候,採用的是detach的方式,並未真正的銷燬Fragment,僅僅是打算銷燬了View,這就導致FragmentManager中仍舊保留正Fragment的快取:

@Override
public void destroyItem(ViewGroup container, int position, Object object) {
    if (mCurTransaction == null) {
        mCurTransaction = mFragmentManager.beginTransaction();
    }
    // 僅僅detach
    mCurTransaction.detach((Fragment)object);
}
複製程式碼

Transaction.detach函式最終會呼叫FragmentManager的detachFragment函式,將Fragment從當前Activity detach

     public void detachFragment(Fragment fragment, int transition, int transitionStyle) {
    if (!fragment.mDetached) {
        <!--只是detach -->
        fragment.mDetached = true;
        if (fragment.mAdded) {
        <!--如果是被added 從added列表中移除-->
            if (mAdded != null) {
                mAdded.remove(fragment);
            }
            ...
            fragment.mAdded = false;
            <!--將狀態設定為Fragment.CREATED-->
            moveToState(fragment, Fragment.CREATED, transition, transitionStyle, false);
        }
    }
}
複製程式碼

可以看到,這裡僅僅會將Fragment設定為Fragment.CREATED,對於Fragment.CREATED狀態的Fragment,FragmentManager是不會呼叫makeInactive進行清理的,

void moveToState(Fragment f, int newState, int transit, int transitionStyle,
        boolean keepActive) {
        ...
 case Fragment.CREATED:
         if (newState < Fragment.CREATED) {
             ...
                if (!keepActive) {
                    if (!f.mRetaining) {
                        makeInactive(f);
                    } else {
                        f.mActivity = null;
                        f.mParentFragment = null;
                        f.mFragmentManager = null;
                    }
               ...
複製程式碼

因為只有makeInactive才會清理Fragment的引用如下:

void makeInactive(Fragment f) {
    if (f.mIndex < 0) {
        return;
    }
    <!--置空mActive列表對於Fragment的強引用-->
    mActive.set(f.mIndex, null);
    if (mAvailIndices == null) {
        mAvailIndices = new ArrayList<Integer>();
    }
    mAvailIndices.add(f.mIndex);
    mActivity.invalidateFragment(f.mWho);
    f.initState();
}
複製程式碼

可見,Fragment的快取仍舊留在FragmentManager中。新的FragmentPagerAdapter被設定後,會通過instantiateItem函式來獲取Fragment,這個時候它首先會從FragmentManager的快取中去取Fragment,取到的Fragment其實就是之前未銷燬的Fragment,這也是為什麼不會重新整理的原因:

@Override
public Object instantiateItem(ViewGroup container, int position) {
	<!--新建一個事務-->
    if (mCurTransaction == null) {
        mCurTransaction = mFragmentManager.beginTransaction();
    }
    final long itemId = getItemId(position);
    <!--利用id與container的id建立name-->
    String name = makeFragmentName(container.getId(), itemId);
    <!--根據name在Activity的FragmentManager中查詢快取Fragment-->
    Fragment fragment = mFragmentManager.findFragmentByTag(name);
    <!--如果找到的話,直接使用當前Fragment-->
    if (fragment != null) {
        mCurTransaction.attach(fragment);
    } else {
    <!--如果找不到則新建,並新建name,新增到container中去-->
        fragment = getItem(position);
        mCurTransaction.add(container.getId(), fragment,
                makeFragmentName(container.getId(), itemId));
    }
    if (fragment != mCurrentPrimaryItem) {
        fragment.setMenuVisibility(false);
        fragment.setUserVisibleHint(false);
    }
   return fragment;
}
複製程式碼

從上面程式碼可以看到,在新建Fragment物件的時候,首先是通過mFragmentManager.findFragmentByTag(name);查詢是否已經有Fragment快取,第二次設定Adapter的時候,由於部分Fragment已經被新增到FragmentManager的快取中去了,新的Adapter仍然能通過mFragmentManager.findFragmentByTag(name)找到快取Fragment,阻止了Fragment的新建,因此不會有整體重新整理的效果。那如果想要整體重新整理怎麼辦呢?可以使用FragementStatePagerAdapter,兩者對於Fragment的快取管理不同。

ViewPager+FragementStatePagerAdapter可以通過setAdapter做到整體重新整理

同樣先看一下FragementStatePagerAdapter的destroyItem函式,FragementStatePagerAdapter在destroyItem的時候使用的是remove的方式,這種方式對於沒有新增到回退棧的Fragment操作來說,不僅會銷燬view,還會銷燬Fragment。

@Override
public void destroyItem(ViewGroup container, int position, Object object) {
    Fragment fragment = (Fragment)object;

    if (mCurTransaction == null) {
        mCurTransaction = mFragmentManager.beginTransaction();
    }
   while (mSavedState.size() <= position) {
        mSavedState.add(null);
    }
    mSavedState.set(position, mFragmentManager.saveFragmentInstanceState(fragment));
    <!--FragementStatePagerAdapter先清理自己的快取-->
    mFragments.set(position, null);
    <!--直接刪除-->
    mCurTransaction.remove(fragment);
}
複製程式碼

可見FragementStatePagerAdapter會首先通過mFragments.set(position, null)清理自己的快取,然後,通過Transaction.remove清理在FragmentManager中的快取,Transaction.remove最終會呼叫FragmentManager的removeFragment函式:

public void removeFragment(Fragment fragment, int transition, int transitionStyle) {
<!-- 其實兩者的主要區別就是看是否在回退棧,如果在,表現就一致,如果不在,表現不一致-->
    final boolean inactive = !fragment.isInBackStack();
    if (!fragment.mDetached || inactive) {
        if (mAdded != null) {
            mAdded.remove(fragment);
        }
        ...
        fragment.mAdded = false;
        fragment.mRemoving = true;
        <!--將狀態設定為Fragment.CREATED或者Fragment.INITIALIZING-->
        moveToState(fragment, inactive ? Fragment.INITIALIZING : Fragment.CREATED,
                transition, transitionStyle, false);
    }
}
複製程式碼

FragementStatePagerAdapter中的Fragment在新增的時候,都沒有addToBackStack,所以moveToState會將狀態設定為Fragment.INITIALIZING ,

void moveToState(Fragment f, int newState, int transit, int transitionStyle,
        boolean keepActive) {
        ...
 case Fragment.CREATED:
         if (newState < Fragment.CREATED) {
             ...
                if (!keepActive) {
                    if (!f.mRetaining) {
                        makeInactive(f);
                    } else {
                        f.mActivity = null;
                        f.mParentFragment = null;
                        f.mFragmentManager = null;
                    }
               ...
複製程式碼

Fragment.INITIALIZING < Fragment.CREATED,這裡一般會呼叫makeInactive函式清理Fragment的引用,這裡其實就算銷燬了Fragment在FragmentManager中的快取。

ViewPager通過populate因此再次新建的時候,FragementStatePagerAdapter的instantiateItem 一定會新建Fragment,因為之前的Fragment已經被清理掉了,在自己的Fragment快取列表中取不到,就新建。看如下程式碼:

    @Override
public Object instantiateItem(ViewGroup container, int position) {
	<!--檢視FragementStatePagerAdapter中是否有快取的Fragment,如果有直接返回-->
    if (mFragments.size() > position) {
        Fragment f = mFragments.get(position);
        if (f != null) {
            return f;
        }
    }
   ...
	 <!--關鍵點   如果在FragementStatePagerAdapter找不到,直接新建,不關心FragmentManager中是否有-->
    Fragment fragment = getItem(position);
    <!--檢視是否需恢復,如果需要,則恢復-->
    if (mSavedState.size() > position) {
        Fragment.SavedState fss = mSavedState.get(position);
        if (fss != null) {
            fragment.setInitialSavedState(fss);
        }
    }
    ...
    mFragments.set(position, fragment);
    mCurTransaction.add(container.getId(), fragment);

    return fragment;
}
複製程式碼

從上面程式碼也可以看出,FragementStatePagerAdapter在新建Fragment的時候,不會去FragmentMangerImpl中去取,而是直接在FragementStatePagerAdapter的快取中取,如果取不到,則直接新建Fragment,如果通過setAdapter設定了新的FragementStatePagerAdapter,一定會新建所有的Fragment,就能夠達到整體重新整理的效果。

FragmentPagerAdapter如何通過notifyDataSetChanged重新整理ViewPager

FragmentPagerAdapter中的資料發生改變時,往往要重新將資料設定到Fragment,或者乾脆新建Fragment,而對於用FragmentPagerAdapter的ViewPager來說,只是利用其notifyDataSetChanged是不夠的,跟蹤原始碼會發現,notifyDataSetChanged最終會呼叫ViewPager中的dataSetChanged:

notifyDataSetChanged流程

void dataSetChanged() {
    ...
    for (int i = 0; i < mItems.size(); i++) {
        final ItemInfo ii = mItems.get(i);
        final int newPos = mAdapter.getItemPosition(ii.object);
       if (newPos == PagerAdapter.POSITION_UNCHANGED) {
            continue;
        }
       if (newPos == PagerAdapter.POSITION_NONE) {
            mItems.remove(i);
            i--;
           ...
           mAdapter.destroyItem(this, ii.position, ii.object);
            needPopulate = true;
           ...
            continue;
        }
    ...
    if (needPopulate) {
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            if (!lp.isDecor) {
                lp.widthFactor = 0.f;
            }
        }
        setCurrentItemInternal(newCurrItem, false, true);
        requestLayout();
    }
}
複製程式碼

預設情況下FragmentPagerAdapter中的getItemPosition返回的是PagerAdapter.POSITION_UNCHANGED,所以這裡不會 destroyItem,即時設定了PagerAdapter.POSITION_NONE,呼叫了其destroyItem,也僅僅是detach,銷燬了View,Fragment仍舊不會重建,必須手動更改引數才可以,這個時機在哪裡呢?FragmentAdapter的getItem函式會在第一次需要建立Fragment的時候呼叫,如果需要將引數傳遞給Fragment,可以通過Fragment.setArguments()來設定,但是僅僅在getItem新建的時候有效,一旦被Fragment被建立,就會被FragmentManager快取,如果不主動釋放,對於當前位置的Fragment來說,getItem函式是不會再次被呼叫的,原因已經在上文的instantiateItem函式處說明了,它會首先去快取中取。那這個時候,如何更新呢?Fragment.setArguments是不能再呼叫的,因為被attach過的Fragment來說不能再次通過setArguments被設定引數,否則丟擲異常

public void setArguments(Bundle args) {
    if (mIndex >= 0) {
        throw new IllegalStateException("Fragment already active");
    }
    mArguments = args;
}
複製程式碼

那如果真要更改就需要在其instantiateItem的時候,通過額外的介面手動設定,同時也必須將getItemPosition返回值設定為POSITION_NONE,這樣才會每次都走View的新建流程,才有可能重新整理:

public int getItemPosition(Object object) {
    return POSITION_NONE;
}
複製程式碼

至於引數如何設定呢?這裡就需要使用者手動提供介面變更引數了,在自定義的FragmentAdapter覆蓋instantiateItem,自己手動獲取快取Fragment,在attach之前,將引數給重新設定進去,之後,Fragment在走onCreateView流程的時候,就會獲取到新的引數。

@Override
public Object instantiateItem(ViewGroup container, int position) {

    String name = makeFragmentName(container.getId(), position);
    Fragment fragment =((FragmentActivity) container.getContext()).getSupportFragmentManager().findFragmentByTag(name);

    if(fragment instanceof MyFragment){
        Bundle bundle=new Bundle();
        bundle.putString("msg",""+System.currentTimeMillis());
        ( (MyFragment) fragment).resetArgument(bundle);
    }

    return super.instantiateItem(container, position);
}

private static String makeFragmentName(int viewId, long id) {
    return "android:switcher:" + viewId + ":" + id;
}
複製程式碼

如此,便可以完成FragmentPagerAdapter中Fragment的重新整理。並且到這裡我們也知道了,對於FragmentPagerAdapter來說,使用者完全不需要自己快取Fragment,只需要快取View,因為FragmentPagerAdapter不會銷燬Fragment,也不會銷燬FragmentManager中快取的Fragment,至於快取的View要不要重新整理,可能就要你具體的業務需求了。

FragmentStatePagerAdapter如何通過notifyDataSetChanged重新整理ViewPager頁面

對於FragmentStatePagerAdapter相對容易些,如果不需要考慮效率,重建所有的Fragment即可,只需要複寫其getItemPosition函式

public int getItemPosition(Object object) {
    return POSITION_NONE;
}
複製程式碼

因為FragmentStatePagerAdapter中會真正的remove Fragment,達到完全重建的效果。

Fragmentmanager Transaction棧的意義

最後看一下Fragmentmanager中Transaction棧,FragmentManager的Transaction棧到底是做什麼的呢?FragmentManager對於Fragment的操作是分批量進行的,在一個Transaction中有多個add、remove、attach操作,Android是有返回鍵的,為了支援點選返回鍵恢復上一個場景的操作,Android的Fragment管理引入Transaction棧,更方便回退,其實將一個Transaction的操作全部翻轉:新增變刪除、attach變detach,反之亦然。對於每個入棧的Transaction,都是需要出棧的,而且每個操作都有前後文,比如進入與退出的動畫,當需要翻轉這個操作,也就是點選返回鍵的時候,需要知道如何翻轉,也就是需要記錄當前場景,對於remove,如果沒有入棧操作,說明不用記錄上下文,可以直接清理掉。對於ViewPager在使用FragmentPagerAdapter/FragmentStatePagerAdapter的時候都不會addToBackStack,這也是為什麼detach跟remove有時候表現一致或者不一致的原因。簡單看一下出棧操作,其實就是將原來從操作翻轉一遍,當然,並不是完全照搬,還跟當前的Fragment狀體有關。

public void popFromBackStack(boolean doStateMove) {
   Op op = mTail;
    while (op != null) {
        switch (op.cmd) {
            case OP_ADD: {
                Fragment f = op.fragment;
                f.mNextAnim = op.popExitAnim;
                mManager.removeFragment(f,
                        FragmentManagerImpl.reverseTransit(mTransition),
                        mTransitionStyle);
            } break;
            case OP_REPLACE: {
                Fragment f = op.fragment;
                if (f != null) {
                    f.mNextAnim = op.popExitAnim;
                    mManager.removeFragment(f,
                            FragmentManagerImpl.reverseTransit(mTransition),
                            mTransitionStyle);
                }
                if (op.removed != null) {
                    for (int i=0; i<op.removed.size(); i++) {
                        Fragment old = op.removed.get(i);
                        old.mNextAnim = op.popEnterAnim;
                        mManager.addFragment(old, false);
                    }
                }
            } break;
            ...
複製程式碼

FragmentManager對於Fragment的快取管理

FragmentManager主要維護三個重要List,一個是mActive Fragment列表,一個是mAdded FragmentList,還有個BackStackRecord回退棧

ArrayList<Fragment> mActive;
ArrayList<Fragment> mAdded;
ArrayList<BackStackRecord> mBackStack;
複製程式碼

mAdded列表是被當前新增到Container中去的,而mActive是全部參與的Fragment集合,只要沒有被remove,就會一致存在,可以認為mAdded的Fragment都是活著的,而mActive的Fragment卻可能被處決,並被置null,只有makeInactive函式會這麼做。

void makeInactive(Fragment f) {
    if (f.mIndex < 0) {
        return;
    }
    mActive.set(f.mIndex, null);
    if (mAvailIndices == null) {
        mAvailIndices = new ArrayList<Integer>();
    }
    mAvailIndices.add(f.mIndex);
    mActivity.invalidateFragment(f.mWho);
    f.initState();
}
複製程式碼

FragmentPagerAdapter獲取試圖獲取的Fragment就是從這兩個列表中讀取的 。

public Fragment findFragmentByTag(String tag) {
    if (mAdded != null && tag != null) {
        for (int i=mAdded.size()-1; i>=0; i--) {
            Fragment f = mAdded.get(i);
            if (f != null && tag.equals(f.mTag)) {
                return f;
            }
        }
    }
    if (mActive != null && tag != null) {
        for (int i=mActive.size()-1; i>=0; i--) {
            Fragment f = mActive.get(i);
            if (f != null && tag.equals(f.mTag)) {
                return f;
            }
        }
    }
    return null;
}    
複製程式碼

總結

本文簡單分析了下ViewPager在使用FrgmentPagerAdapter跟FragmentStatePagerAdapter遇到問題,原理、及問題的解決方案。

作者:看書的小蝸牛 原文連結:ViewPager重新整理問題原理分析及解決方案(FragmentPagerAdapter+FragementStatePagerAdapter)

僅供參考,歡迎指正

相關文章