ViewPager+Fragment 更新問題


先說一下我的問題場景:

我的主Activity使用ViewPager+4個fragment的方式實現,自定義適配器MyMainFragmentPagerAdapter繼承自FragmentPagerAdapter,MainActivity繼承自FragmentActivity。

實際使用的時候,當MainActivity執行完一些必要數據後,開始爲ViewPager設置適配器資源,各個Fragment的實例化是在FragmentPagerAdapter的getItem方法中執行的。程序啓動的時候首先會加載第一個和第二個Fragment(即調用getItem實例化)。

目前操作是,按home鍵,整個APP進入後臺,去系統設置頁面切換語言,語言切換完成後,再次進入我的APP。

結果MainActivity執行了onDestroy方法,第一個Fragment依次執行了onDestroyView-->onDestroy-->onDetach,緊接着執行了Fragment的onCreate方法,然後纔去執行了MainAct的onCreate方法,這點很奇怪,不明白爲什麼會直接就執行Fragment的onCreate方法,按照我的理解,不應該是MainActivity和裏面的Fragment都已經銷燬了嗎?Fragment怎麼優先於宿主MainActivity執行呢?(這個問題暫時擱置,希望有大神看到能指導下)

這樣一來,之前所有的正常邏輯都打亂了,因爲我的fragment創建的時候需要帶一些參數,而像剛纔這種執行順序的話,我的參數就無法傳遞到Fragment了。那麼我是不是可以來自己控制這個Fragment的創建呢,把已經加載的Fragment清除掉,加入一個新的Fragment?該怎麼做呢?如何像ViewPager一樣控制Fragment呢?

在使用中我們會發現,連接ViewPager和Fragment的適配器PagerAdapter做了很多工作,那麼相應的控制應該就是在這個類裏面做了。

首先來看看PagerAdapter:

【PagerAdapter】
 
PageAdapter 是 ViewPager 的支持者,ViewPager 將調用它來取得所需顯示的頁,而 PageAdapter 也會在數據變化時,通知 ViewPager。這個類也是FragmentPagerAdapter 以及 FragmentStatePagerAdapter 的基類。如果繼承自該類,至少需要實現 instantiateItem(), destroyItem(), getCount() 以及 isViewFromObject()。
 
getItemPosition()
該函數用以返回給定對象的位置,給定對象是由 instantiateItem() 的返回值。
在 ViewPager.dataSetChanged() 中將對該函數的返回值進行判斷,以決定是否最終觸發 PagerAdapter.instantiateItem() 函數。
在 PagerAdapter 中的實現是直接傳回 POSITION_UNCHANGED。如果該函數不被重載,則會一直返回 POSITION_UNCHANGED,從而導致 ViewPager.dataSetChanged() 被調用時,認爲不必觸發 PagerAdapter.instantiateItem()。很多人因爲沒有重載該函數,而導致調用
PagerAdapter.notifyDataSetChanged() 後,什麼都沒有發生。
instantiateItem()
在每次 ViewPager 需要一個用以顯示的 Object 的時候,該函數都會被 ViewPager.addNewItem() 調用。
notifyDataSetChanged()
在數據集發生變化的時候,一般 Activity 會調用 PagerAdapter.notifyDataSetChanged(),以通知 PagerAdapter,而 PagerAdapter 則會通知在自己這裏註冊過的所有 DataSetObserver。其中之一就是在 ViewPager.setAdapter() 中註冊過的 PageObserver。PageObserver 則進而調用 ViewPager.dataSetChanged(),從而導致 ViewPager 開始觸發更新其內含 View 的操作。

 


實際使用過程中,我們不太會用到這個基類,而是使用如下兩個派生類:


【FragmentPagerAdapter】
 
FragmentPagerAdapter 繼承自 PagerAdapter。相比通用的 PagerAdapter,該類更專注於每一頁均爲 Fragment 的情況。如文檔所述,該類內的每一個生成的 Fragment 都將保存在內存之中,因此適用於那些相對靜態的頁,數量也比較少的那種;如果需要處理有很多頁,並且數據動態性較大、佔用內存較多的情況,應該使用FragmentStatePagerAdapter。FragmentPagerAdapter 重載實現了幾個必須的函數,因此來自 PagerAdapter 的函數,我們只需要實現 getCount(),即可。且,由於 FragmentPagerAdapter.instantiateItem() 的實現中,調用了一個新增的虛函數 getItem(),因此,我們還至少需要實現一個 getItem()。因此,總體上來說,相對於繼承自 PagerAdapter,更方便一些。
 
getItem()
該類中新增的一個虛函數。函數的目的爲生成新的 Fragment 對象。重載該函數時需要注意這一點。在需要時,該函數將被 instantiateItem() 所調用。
如果需要向 Fragment 對象傳遞相對靜態的數據時,我們一般通過 Fragment.setArguments() 來進行,這部分代碼應當放到 getItem()。它們只會在新生成 Fragment 對象時執行一遍。
如果需要在生成 Fragment 對象後,將數據集裏面一些動態的數據傳遞給該 Fragment,那麼,這部分代碼不適合放到 getItem() 中。因爲當數據集發生變化時,往往對應的 Fragment 已經生成,如果傳遞數據部分代碼放到了 getItem() 中,這部分代碼將不會被調用。這也是爲什麼很多人發現調用 PagerAdapter.notifyDataSetChanged() 後,getItem() 沒有被調用的一個原因。
instantiateItem()
函數中判斷一下要生成的 Fragment 是否已經生成過了,如果生成過了,就使用舊的,舊的將被 Fragment.attach();如果沒有,就調用 getItem() 生成一個新的,新的對象將被 FragmentTransation.add()。
FragmentPagerAdapter 會將所有生成的 Fragment 對象通過 FragmentManager 保存起來備用,以後需要該 Fragment 時,都會從 FragmentManager 讀取,而不會再次調用 getItem() 方法。
如果需要在生成 Fragment 對象後,將數據集中的一些數據傳遞給該 Fragment,這部分代碼應該放到這個函數的重載裏。在我們繼承的子類中,重載該函數,並調用 FragmentPagerAdapter.instantiateItem() 取得該函數返回 Fragment 對象,然後,我們該 Fragment 對象中對應的方法,將數據傳遞過去,然後返回該對象。
否則,如果將這部分傳遞數據的代碼放到 getItem()中,在 PagerAdapter.notifyDataSetChanged() 後,這部分數據設置代碼將不會被調用。
destroyItem()
該函數被調用後,會對 Fragment 進行 FragmentTransaction.detach()。這裏不是 remove(),只是 detach(),因此 Fragment 還在 FragmentManager 管理中,Fragment 所佔用的資源不會被釋放。
 
 
【FragmentStatePagerAdapter】
 
 
FragmentStatePagerAdapter 和前面的 FragmentPagerAdapter 一樣,是繼承子 PagerAdapter。但是,和 FragmentPagerAdapter 不一樣的是,正如其類名中的 'State' 所表明的含義一樣,該 PagerAdapter 的實現將只保留當前頁面,當頁面離開視線後,就會被消除,釋放其資源;而在頁面需要顯示時,生成新的頁面(就像 ListView 的實現一樣)。這麼實現的好處就是當擁有大量的頁面時,不必在內存中佔用大量的內存。
 
getItem()
一個該類中新增的虛函數。
函數的目的爲生成新的 Fragment 對象。
Fragment.setArguments() 這種只會在新建 Fragment 時執行一次的參數傳遞代碼,可以放在這裏。
由於 FragmentStatePagerAdapter.instantiateItem() 在大多數情況下,都將調用 getItem() 來生成新的對象,因此如果在該函數中放置與數據集相關的 setter 代碼,基本上都可以在 instantiateItem() 被調用時執行,但這和設計意圖不符。畢竟還有部分可能是不會調用 getItem() 的。因此這部分代碼應該放到 instantiateItem() 中。
instantiateItem()
除非碰到 FragmentManager 剛好從 SavedState 中恢復了對應的 Fragment 的情況外,該函數將會調用 getItem() 函數,生成新的 Fragment 對象。新的對象將被 FragmentTransaction.add()。
FragmentStatePagerAdapter 就是通過這種方式,每次都創建一個新的 Fragment,而在不用後就立刻釋放其資源,來達到節省內存佔用的目的的。
destroyItem()
將 Fragment 移除,即調用 FragmentTransaction.remove(),並釋放其資源。

 

 

結合實際情況,我這裏就使用了FragmentPagerAdapter。
從實際項目中我們可以知道,ViewPager一般會緩存當前Fragment和兩邊相鄰的Fragment。即當我程序啓動的時候,會默認加載了第一個fragment和右邊的fragment(即第二個),而當我切換到第二個fragment的時候,又會自動加載了第三個fragment,而當我繼續切換到第三個fragment的時候,會自動加載第四個fragment,並且此時,第一個

fragment執行了onDestroyView。這些fragment的生命週期函數是由FragmentPagerAdapter來控制,具體就是在相應的時候調用了instantiateItem和destroyItem方法。在切換不同 的fragment的時候,會反覆執行這兩個方法,但是getItem卻很少執行。

我們進入FragmentPagerAdapter源碼看下它在instantiateItem執行了什麼:

 

@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;
    }


從源碼中可以看到,getItem方法是由instantiateItem調用的,而fragment對象是由FragmentManager管理的,一旦fragment對象創建了,對應我的APP就是四個fragment創建了,無論怎麼切換都不會再執行getItem方法了,這樣也就無法重新創建fragment對象了。這樣也就可以解釋了爲什麼我的activity再次執行onCreate的時候,沒有去重新執行創建fragment的過程。在重新啓動activity的過程中,很可能當前應用的FragmentManager並沒有銷燬,這個FragmentManager把新的activity和已經存在的fragment又關聯起來了(但這又是爲什麼?是什麼原理的?不懂!)。


經過上面的分析,似乎弄明白了重啓activity的時候沒有重新創建fragment對象的原因,那麼我現在想把這個已經存在的fragment清除掉,讓我來控制重新創建一個新的fragment關聯到這個activity。
其實我們可以仿照instantiateItem源碼執行的過程,這個源碼在fragment爲空的時候,執行了創建和關聯的動作。那麼我在這裏強制執行創建和關聯動作:

首先複寫MyMainFragmentPagerAdapter的instantiateItem方法:

@Override
    public Object instantiateItem(ViewGroup container, int position) {
        Log.i(TAG, "instantiateItem:"+position);
        Fragment fragment = (Fragment) super.instantiateItem(container,
                position);//得到緩存的fragment
            
        
        if(update && position==0){//我這裏只需要重新更新第一個fragment
            Log.w(TAG, "update new fragment");
                //得到tag,這點很重要
                String fragmentTag = fragment.getTag();
 
                FragmentTransaction ft = fm.beginTransaction();
                //移除舊的fragment
                ft.remove(fragment);
                //換成新的fragment
                fragment = getItem(position);
                //添加新fragment時必須用前面獲得的tag,這點很重要
                ft.add(container.getId(), fragment, fragmentTag);
                ft.attach(fragment);
                ft.commit();
                update = false;//清除更新標記(只有重新啓動的時候需要去創建新的fragment對象),防止正常情況下頻繁創建對象
        }
        
        
        return fragment;
        
    }


然後,當我在需要更新fragment的時候,就調用一下PagerAdapter的notifyDataSetChanged()方法,這個方法會去執行instantiateItem方法

這樣我就可以實現在重啓Activity的時候,在需要的時候手動重新創建一個新的fragment了。

當然了,這裏還遺留了一些問題,希望以後能搞明白,本質原因還是需要進一步探究啊:

當應用進入後臺的時候,切換了系統語言,爲什麼會導致再次進入應用的時候重新創建Activity呢?

宿主activity已經執行了onDestroy,而fragment也相應的執行到了生命末尾,爲什麼還沒等宿主activity執行onCreate的時候,fragment就已經開始執行onCreate了?

FragmentManager不會隨着宿主activity的銷燬而消亡嗎,它是怎麼關聯activity和fragment的?

文章寫的有點亂,只是爲了記錄這個疑難問題,方便以後可以查閱深究。有大神能給點指導就更好了。

最後貼上兩篇閱讀到的文章,問題得以解決還是這兩篇文章的功勞,3Q:

android FragmentPagerAdapter getItem方法沒有執行

FragmentPagerAdapter刷新fragment最完美解決方案

補充:

鑑於我的應用場景,如果我的應用不需要跟隨系統語言重新加載資源的話,可以在Manifest.xml中的MainActivity節點添加屬性:

android:configChanges="locale|layoutDirection"

這樣設置後,當系統語言發生了改變,在進入Activity時會觸發onConfigurationChanged方法,而不會重新執行onCreate方法。相反如果不設置這個屬性則會重新執行onCreate方法。(這樣做,上面那些所做的修改也就沒必要了,一個配置就搞定了~)

注意,在4.2以上的版本中需要添加layoutDirection,解釋:android:configChanges locale 改語言後,該配置不起作用的原因


4.2增加了一個layoutDirection屬性,當改變語言設置後,該屬性也會成newConfig中的一個mask位。所以ActivityManagerService(實際在ActivityStack)在決定是否重啓Activity的時候總是判斷爲重啓。
需要在android:configChanges 中同時添加locale和layoutDirection。


當然如果應用支持多語言,想跟隨系統語言的話,重啓還是很有必要的。

尊重作者勞動成果:https://blog.csdn.net/anhenzhufeng/article/details/48468167

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章