1. 程式人生 > >深入一點 讓細節幫你和Fragment更熟絡

深入一點 讓細節幫你和Fragment更熟絡

有一段時間沒有寫部落格了,作為2017年的第一篇,初衷起始於前段時間一個接觸安卓開發還不算太長時間的朋友聊到的一個問題:
“假設,想要對一個Fragment每次在隱藏/顯示之間做狀態切換時進行監聽, 從而在這個時候去完成一些操作,應該怎麼去實現呢?”
相信大家聽到這類問題第一反應都會覺得是很容易的。而又經過一番討論過後,發現他的問題場景相對來說比較特殊一點的是:
其想要監聽的Fragment是巢狀在另一層Fragment內的子Fragment。這就更有趣了一點,當然了,這個需求場景同樣也不會太難實現。
既然這樣還寫什麼部落格呢?哈哈。問題本身雖然不算難解決,但個人發現在對其解決的過程中,其實能涉及到不少對於Fragment較實用的小細節。
那麼,自己也可以剛好藉此機會,重新更加深入的回顧、整理和總結一下關於Fragment的一些使用細節和技巧。何樂而不為呢?特此記錄。

問題場景還原

場景還原

如上圖所示,這一圖例基本上包含了現在大多數主流APP常用的一種UI設計模式。有底部導航,有ViewPager,有側拉選單等等。
其實對於這種UI模式,有一個非常直觀的印象就是“碎片化”。那麼,對應到Android中,用Fragment去實現這種設計就再合適不過了。

那麼,我們也就可以看到:在這裡,使用者的一系列操作就會涉及到大量的Fragment的隱藏和顯示的狀態切換工作。
從而提歸正傳,我們試圖在這一圖例中去模擬的還原一下之前說到的那個問題。首先,我們來分解一下這個用例中的UI設計:

  • 首先自然是主介面,主介面是一個Activity。Ac中有一個底部導航,分為三頁,三頁自然分別對應了三個Fragment。
  • 第二頁和第三頁的Fragment介面,我沒有去新增額外的設計了,所以十分一目瞭然,故不加贅述。
  • 重點在第一頁,可以看到這一頁中有一個側滑選單,側滑選單裡的選項又對應了另外的Fragment介面。
  • 那麼,很顯然的,側滑選單裡的介面,就是巢狀在底部導航的第一頁Fragment裡的另一層Fragment了。
  • 最後,我們可以看到巢狀在側滑選單裡的第一個子Fragment,它裡面是一個ViewPager,於是又涉及到兩個新的子Fragment。

OK,到了這裡,有了這一番UI分解,我們有了一個大概的瞭解。現在我們藉助一個實際的常用功能更好的還原我們之前說到的那個問題。
假設在底部導航的“第三頁”介面中,有一個功能叫做“清除快取”。那麼,使用這一功能就意味著:其它介面當中原先快取的資料將被清除。
也就意味著,當用戶再次切換到另外的介面中時(Fragment由隱藏切換到顯示),就需要清除該介面原本的內容,重新獲取最新的內容顯示。

OK,現在已經回到了我們最初說到的話題了。那麼,接著就讓我們以這個例子切入,由易到難的看一下:
在常見的各種情況下,應該如何監聽Fragment的顯示狀態切換。而在這其中,又可以注意哪些關於Fragment比較實用的小細節。

replace與hide/show

在上一節的圖例中,我們說到主介面Activity中有一個底部導航欄,分別對應著三個功能介面(即三個Fragment)。
顯然,我們肯定有兩種方式來控制這種Fragment的導航切換,即使用replace進行切換,或者通過hide/show來控制切換。

以我們的圖例來說,假設我們想要從“第一頁”切換到“第二頁”,那麼我們可以這樣做(replace):

FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.replace(R.id.fragment_container, new SecondFragment());
ft.commit();

當然也可以這樣做(hide/show):

FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();

ft.hide(new FirstFragment())
  .show(new SecondFragment())
  .commit();

但這裡就注意了:如果我們是剛開始接觸Fragment,上面的程式碼看上去似乎沒問題,但實際肯定是不能這樣去使用hide/show的。
因為就像上述程式碼表述的一樣,我們一定要留意到“new ”,它看上去就像在說:每次隱藏和顯示的都是一個全新的Fragment物件。
而事實上也的確如此。所以,這也就意味著:如果這麼搞,我們肯定是沒辦法正確的控制fragment的切換顯示的。

那麼,我們應該怎麼去完成這種需求呢?實際可以提供兩種方式,第一種就是為Fragment新增“單例”。
以我們圖例中顯示的來說,當進入主介面後,優先顯示的是“第一頁的介面”,所以我們可以先讓它顯示:

FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();

ft.add(R.id.fragment_container, FirstFragment.getInstance());
ft.add(R.id.fragment_container, SecondFragment.getInstance());
ft.add(R.id.fragment_container, ThirdFragment.getInstance());
ft.hide(SecondFragment.getInstance());
ft.hide(ThirdFragment.getInstance());

ft.commit();

於是在此之後,由“第一頁”的Fragment切換到“第二頁”的工作,則可以通過類似下面的程式碼來實現:

FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();

ft.hide(FirstFragment.getInstance())
  .show(SecondFragment.getInstance())
  .commit();

你肯定注意到,這裡我選擇先將會使用到的Fragment物件都新增到記憶體中去,讓暫時不需要顯示的碎片先hide。
需要額外說明的是: 這種做法本身其實也是可行的,但在以上的用例裡我確實是省方便,而選擇了這樣的做法。而實際上來說:
個人覺得,如果我們其實並沒有讓暫時不需要顯示的Fragment進行“預載入”的需求的話。那麼對應來說:
選擇在真正需要切換Fragment顯示的時候,再將要顯示的Fragment物件進行add,然後控制它們hide和show是更好的做法。
而之所以說這樣做更好的原因是什麼呢?我們稍微放一放,在之後不久的分析裡我們就可以看到。

上述的“單例”這種方式能完成我們的需求嗎?當然是可以的。但讓人滿意嗎?似乎總覺得有些彆扭。
的確如此,這種方式更像是用Java的方式去解決問題,而非使用Android的方式來解決問題。
所以,我們接著看第二種方式,即使用FragmentManager自身來管理我們的Fragment物件。

首先,我們要知道,通過FragmentManager開啟事務來動態新增Fragment物件的時候,也是可以為Fragment設定標識的。

FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();

FirstFragment first = new FirstFragment();
SecondFragment second = new SecondFragment();
ThirdFragment third = new ThirdFragment();

ft.add(R.id.fragment_container,first, "Tab0");
ft.add(R.id.fragment_container,second,"Tab1");
ft.add(R.id.fragment_container,third,"Tab2");

ft.hide(second);
ft.hide(third);

ft.commit();

上述程式碼中的“Tab0”這種東西就是我們為Fragment物件設定的標識(Tag),其好處就在於我們之後能夠非常方便的控制不同Fragment的切換。

//這裡是導航切換的回撥
public void onTabSelected(int position) {
    if(position != currentFragmentIndex)
    {
        FragmentManager fm = getSupportFragmentManager();
        FragmentTransaction ft = fm.beginTransaction();

        ft.hide(fm.findFragmentByTag("Tab"+currentFragmentIndex))
          .show(fm.findFragmentByTag("Tab"+position))
          .commit();

         currentFragmentIndex = position;
     }
 }

就像這裡做的,其實FragmentManager本身就有一個List來存放我們add的Fragment物件。這意味著:
我們通過設定的Tag,可以直接複用FragmentManager中已經add過的Fragment物件,而無需所謂的“單例”。

在上述程式碼中:自定義的currentFragmentIndex用於記錄當前所在的導航頁索引,position則意味著要切換顯示的介面的索引。
那麼,再配合上我們對Fragment物件進行add的時候設定的Tag,便能非常方便簡潔的實現Fragment的切換顯示了。

回到之前說的,我們也可以選擇不一次性將三個fragment進行add,而是做的更極致一點。而原理依然非常簡單:

            public void onTabSelected(int position) {
                if (position != currentFragmentIndex) {
                    FragmentManager fm = getSupportFragmentManager();
                    FragmentTransaction ft = fm.beginTransaction();

                    Fragment targetFragment = fm.findFragmentByTag("Tab" + position);
                    if (targetFragment == null) {
                        switch (position) {
                            case 1:
                                targetFragment = new SecondFragment();
                                break;
                            case 2:
                                targetFragment = new ThirdFragment();
                                break;
                        }
                         ft.add(R.id.fragment_container,targetFragment,"Tab"+position);
                    }

                    ft.hide(fm.findFragmentByTag("Tab"+currentFragmentIndex))
                      .show(targetFragment)
                      .commit();

                    currentFragmentIndex = position;
                }
      }

與之前不同的就是:現在我們最初只add需要顯示的FirstFragment。然後在切換Fragment顯示的時候,首先通過findFragmentByTag查詢對應的Fragment物件是否已經被add進了記憶體,如果沒有則新建該物件,並進行add。而如果已經存在,則可以直接進行復用,並控制隱藏與顯示的切換了。

好了,到這裡我們看到通過這兩種方式都可以實現切換Fragment顯示的需求。那麼,其差別在哪裡呢?我們可以簡單的到原始碼中找下答案。

從原始碼看replace與add的異同

首先,我們可以明確一點的是:無論是使用到replace還是使用hide/show,前提都需要確保將相關的Fragment物件放入FragmentManager。
那麼,在之前的用例描述中:對於hide/show的使用來說,我們知道首先是通過add的方式來進行的。就像如下程式碼所做的這樣:

ft.add(R.id.fragment_container, first, "Tab0");

那麼,對於replace來說又是如何呢?其實我們可以開啟replace方法的原始碼看一看說明:

以上截圖是原始碼中對於replace方法的註釋說明,我們閱讀一下發現:簡單的來說,它似乎是在告訴我們,呼叫replace方法,效果基本就等同於:
先呼叫remove方法刪除掉指定containerViewId中當前所有已新增(add)的fragment物件,然後再通過add方法將replace傳入的物件新增。

看到這裡,我們難免在想,這麼說其實replace和add在本質上來說,是很相似的,其實這樣說也沒錯。通過以下程式碼可以驗證上面的結論。

Log.d(TAG,getSupportFragmentManager().getFragments().size()+"");

通過以上程式碼可以獲取當前FragmentManager中存放的有效的fragment物件的數量,那麼對於我們上面說到的用例中:

  • 當使用replace控制fragment的顯示時,會發現獲取到的碎片數始終是1。因為每次replace時,都會將之前存在的fragment物件remove掉。
  • 當使用hide/show控制時,獲取到的碎片數將與我們進行add的數量相同。比如之前我們在首頁add了3個fragment,獲取到的數量就是3。

那麼,fragment內部究竟是怎樣的機制,才會造成這樣的結果呢?回顧一下:
其實我們在管理fragment的時候,始終在和兩個東西打交道,那就是:FragmentManager與FragmentTransaction。

FragmentManager其實是一個抽象類,它的具體實現是FragmentManagerImpl,其內部有這樣的東西:

    ArrayList<Fragment> mActive;
    ArrayList<Fragment> mAdded;
    //.......

我們前面說到FragmentManager自身就有一個集合來存放fragment物件,其實就是上面這樣的東西。

FragmentTransaction其實也是一個抽象類,通過FragmentManagerImpl其中的程式碼,我們可以知道其具體實現:

是的,FragmentTransaction的具體實現其實是一個叫做BackStackRecord的類。由此為基礎,我們就可以看看add和replace究竟做了什麼樣的工作。

可以看到add和replace很重要的一個的區別在於某種行為標識:“OP_ADD”與“OP_REPLACE”。
但它們二者最終都是來到一個叫做doAddOp的方法,擷取這個方法內目的性最強的部分程式碼如下:

可以看到這裡做的其實就是建立一個Op型別的物件,然後執行addOp方法。那麼,我們首先看看Op是個什麼東西?

好吧,連我資料結構這麼渣的,也看出這就是一個連結串列結構的東東啦。其實不難理解,因為我們在正式commit事務之前:
其實可以執行一系列的add,replace,hide,remove等等的操作,所以肯定是需要一個類似連結串列這樣的資料結構來更好的記錄這些資訊的。
那麼,至於addOp這個方法就不難想象了,其實就是通過修改連結串列節點資訊來記錄所做的類似add這樣的操作。節省篇幅,就不貼原始碼截圖了。

但問題是,到現在我們還沒有和之前說到的FragmentManager產生聯絡。這是沒錯的,因為真正和FM產生聯絡,自然是在commit之後。

這裡由於能力和篇幅有限,就不會做詳細的逐步分析了,總之我們明白一點:BackStackRecord本身實現了Runnable介面。
接著,在commit之後的一系列相關呼叫之後,最終則會進入到BackStackRecord的run()方法開始執行。

那麼,現在我們擷取run()方法內我們關心的部分程式碼來看看:

              switch (op.cmd) {
                case OP_ADD: {
                    Fragment f = op.fragment;
                    f.mNextAnim = enterAnim;
                    mManager.addFragment(f, false);
                } break;
                case OP_REPLACE: {
                    Fragment f = op.fragment;
                    int containerId = f.mContainerId;
                    if (mManager.mAdded != null) {
                        for (int i = mManager.mAdded.size() - 1; i >= 0; i--) {
                            Fragment old = mManager.mAdded.get(i);
                            if (FragmentManagerImpl.DEBUG) Log.v(TAG,
                                    "OP_REPLACE: adding=" + f + " old=" + old);
                            if (old.mContainerId == containerId) {
                                if (old == f) {
                                    op.fragment = f = null;
                                } else {
                                    if (op.removed == null) {
                                        op.removed = new ArrayList<Fragment>();
                                    }
                                    op.removed.add(old);
                                    old.mNextAnim = exitAnim;
                                    if (mAddToBackStack) {
                                        old.mBackStackNesting += 1;
                                        if (FragmentManagerImpl.DEBUG) Log.v(TAG, "Bump nesting of "
                                                + old + " to " + old.mBackStackNesting);
                                    }
                                    mManager.removeFragment(old, transition, transitionStyle);
                                }
                            }
                        }
                    }
                    if (f != null) {
                        f.mNextAnim = enterAnim;
                        mManager.addFragment(f, false);
                    }
                } break;

首先是OP_ADD,可以看到處理的程式碼非常簡單,關鍵的就是那句:

mManager.addFragment(f, false);

我們回到FragmentManagerImpl來中看看這句程式碼究竟做了什麼:

    public void addFragment(Fragment fragment, boolean moveToStateNow) {
        if (mAdded == null) {
            mAdded = new ArrayList<Fragment>();
        }
        if (DEBUG) Log.v(TAG, "add: " + fragment);
        makeActive(fragment);
        if (!fragment.mDetached) {
            if (mAdded.contains(fragment)) {
                throw new IllegalStateException("Fragment already added: " + fragment);
            }
            mAdded.add(fragment);
            fragment.mAdded = true;
            fragment.mRemoving = false;
            if (fragment.mHasMenu && fragment.mMenuVisible) {
                mNeedMenuInvalidate = true;
            }
            if (moveToStateNow) {
                moveToState(fragment);
            }
        }
    }

這個方法其實也不復雜,關鍵的資訊就是將fragment物件加入mAdded集合中,而makeActive則會將其加入到mActive集合。
(之前說到的通過getFragmentManager.getFragments這句程式碼返回的其實就是mActive這個集合)
而最後的moveToState就是真正關鍵的改變fragment物件狀態的操作了,這個方法比較複雜,這裡不深入分析了。

現在我們回頭繼續看run方法中OPP_REPLACE執行的操作如何,事實上有了之前的基礎,我們不難發現:
replace的不同之處就在於,會先把mManager中mAdded集合內相同contanierViewID的fragment物件遍歷出來刪除掉:

mManager.removeFragment(old, transition, transitionStyle);

然後最後就像我們之前知道的那樣,其實依舊是把fragment物件新增到mManager的集合中去:

if (f != null) {
   f.mNextAnim = enterAnim;
   mManager.addFragment(f, false);
  }

現在我們應該明白,replace和add為什麼會造成在mManger中存放的數量不同以及原始碼中replace方法的註釋說明的原因了。但繼續延伸,
回憶一下:我們知道FragmentTransaction實際上還有一個功能叫做addToBackStack(),故名思議,就是說將Fragment物件加入返回棧。
其實這個方法的實際操作,在之前我們分析過的原始碼中也可以得知。在BackStackRecord的run方法中,對於OPP_REPLACE操作有如下程式碼:

if(mAddToBackStack)就是指使用了addToBackStack方法的情況,這時執行的一個操作是將fragment物件的mBackStackNesting變數自增。
隨後緊接著的操作就是通過mManager去removeFragment,重點就在這裡,讓我們看看removeFragment方法的原始碼:

注意final boolean inactive = !fragment.isInBackStack();這行程式碼,其具體實現為:

也就是說,由於addToBackStack的存在,會導致inactive的獲取結果為false。所以這時根本不會真正去執行if語句塊裡真正刪除fragment物件的操作。
這顯然是符合邏輯的,將fragment物件加入返回棧,意味著我之後還可能會使用到該物件,你自然不能像之前一樣把它們清除掉。
當然,我們還可以自己在addToBackStack使用之後,再通過fragmentManager.getFragments.size()去驗證一下獲取到的fragment物件的數量變化。

生命週期的不同

我們現在已經知道當通過add,replace,remove等操作時,最終會通過moveToState去改變對應fragment物件的狀態。
那麼,hide/show又是如何呢?於是我們又回到了BackStackRecord的run方法當中尋覓答案:

那我們就以hideFragment作為例子,看看這時mManager究竟是做的什麼工作呢?

可以看到這時實際上就告別了moveToState,其本質在於通過改變fragment物件的mView的可視性來控制顯示,並回調onHiddenChanged。

以上的分析的目的為何呢?其實就是為了證明,通過replace和hide/show兩種方式,最大的區別就在於:二者的生命週期變化相去甚遠。
我們還是可以自己去驗證這點。最簡單的方式是:寫一個基類的BaseFragment,然後為所有生命週期回撥新增日誌列印,就類似於如下這樣:

public class BaseFragment extends Fragment{

    protected String clazzName = this.getClass().getSimpleName() +" ==> ";

    @Override
    public void onAttach(Context context) {
        Log.d(clazzName,"onAttach");
        super.onAttach(context);
    }

    //......
}

那麼,當我們使用replace進行切換顯示的時候,會發現其生命週期的路線類似於下面這樣:

然後我們切換到“hide/show”來觀察生命週期的變化,發現其回撥如下所示:

而使用replace時,是否使用addToBackStack的另一個區別也是生命週期上的不同。
沒有使用addToBackStack的時候,被切換的物件和切換進來的物件的生命週期分別為:

  • onPause → onStop → onDestroyView → onDestroy → onDeatch
  • onAttach → onCreate → onCreateView → onViewCreated → onActivityCreated → onStart → onResume

而當使用了addToBackStack後,被切換的物件的生命週期變化則成了:

  • onPause → onStop → onDestroyView

而切換進行的物件,如果是首次進行切換,則與之前無異。反之,如果已經存在於返回棧內,生命週期變化則成了:

  • onCreateView → onViewCreated → onActivityCreated → onStart → onResume

現在我們回到之前說到的一個問題,為什麼說不需要在最初就把潛在的幾個Fragment一股腦進行add,有了之前分析的基礎。
我們知道把fragment進行add過後,最終會執行到moveToState方法進行狀態設定,那對應到我們之前的例子中來說的話:
就代表著我們最初新增的三個fragment物件,都會經歷onAttach → onCreate → …… → onResume這一初始化生命週期。
這意味著新增的三個fragment物件中,“第二頁”與“第三頁”雖然目前不用顯示,但系統需要耗費時間去完成它們的初始化週期。
這顯然在一定程度上會影響效率。當然,具體要怎麼使用其實還是看實際的需求哪種更合適。我們只要明白其中的細節就可以了。

現在,我們思考一個問題,對於我們本文中的圖例應用來說,究竟使用replace還是hide/show更適合呢?其實有了之前的基礎,我們知道:

ft.add(R.id.fragment_container,first, "Tab0");

這行程式碼的效果,其實完全可以用這樣使用replace來轉換:

 ft.replace(R.id.fragment_container,first,"Tab0").addToBackStack(null);

但是,我們前面也說到了,最大的區別就在於二者生命週期變化的不同。再分析一下:

  • 首先,如果我們使用的是replace切換fragment的顯示,那顯然我們需要使用addToBackStack。否則就無需談什麼監聽由隱藏到顯示了,因為單獨replace每次都意味著切換進的是一個全新的fragment物件。
  • 如果使用replace+addToBackStack,那麼被切換的fragment物件(即隱藏的物件)與切換進的fragment物件(即顯示的物件)的生命週期變化路線我們都已經清楚了,這時就有點類似監聽Activity了。
  • 使用hide/show來控制顯示切換,顯然是最簡單的,監聽onHiddenChange回撥,實現自己的目的就可以了。

上述的第2、3種方式都能實現目的,但replace最大的缺陷就在於每次切換都會進入onCreateView到onResume這一週期。
這其中總會涉及到我們在fragment這些生命週期中做出的一些例如資料初始化的操作等,顯然這樣控制起來是非常麻煩的(資料重複載入等)。
所以,綜合比較之下,顯然hide/show才是最合適的方式。而對於replace來說,最適合的顯然就還是那種比較典型的例子:
比如pad上的新聞應用,左邊是新聞列表,右邊為新聞的詳細內容,這時右邊的fragment用replace來切換新聞內容顯示就是最合適的。

那麼回到我們本文之前圖例裡的演示應用,那麼比如在“第三頁”清除快取後,切換到了“第二頁”。
這時使用hide/show切換fragment顯示,然後通過onHiddenChange完成監聽就非常簡單了。
這就是這個圖例裡,針對於我們最初提出的問題可以演示的第一種情況,也是最簡單的一種情況。
當然了,這也是因為圖例中,首頁底部導航對應的三個fragment都隸屬於同一級,即主Activity當中。

getFragmentManager還是getChildFragmentManager()?

現在階段性總結一下,本文圖例中,首頁導航的三個fragment都屬於同一個FragmentMnanger管理。所以根據之前的原始碼分析我們就可以得知:
在我們通過hide/show來切換兩個碎片顯示時,相對應的,它們的onHiddenChaned方法就會被回撥,所以這個時候監聽它們的隱藏/顯示是很容易的。
那我們更進一步,比較特殊的是圖例中“第一頁”的介面,由圖可以看到其中有一個側滑選單,選單中的三個選項卡對又應另外三個fragment。
OK,那麼現在思考一下!這三個fragment還和我們之前說到的首頁導航對應的三個fragment位於同一級別嗎?我們來分析一下。

首先,我們已經說到在程式碼中動態的控制fragment,都藉助於FragmentManager。而對應“第一頁”的FirstFragment本身也是一個fragment。
所以在這個時候,與之前我們在主Activity通過get(Support)FragmentManager有一點不同的是,在Fragment當中我們多了一個選擇:

我們發現有趣的是多了一個叫做getChildFragmentManager的方法,它們之間到底區別在哪呢?在主Activity和FirstFragment分別新增下如下日誌:

// Activity
Log.d(TAG, getSupportFragmentManager()/*或者getFragmentManager()*/+"");
// FirstFragment
Log.d(TAG,getFragmentManager()+"\n"+getChildFragmentManager());

然後執行程式發現如下的日誌列印:

由此我們可以發現,在FirstFragment中獲取的FragmentManager和之前在MainActivity中的是同一物件,歸屬於一個叫做HostCallBacks的東西管理。
與之不同的是:通過getChildFragmentManager獲取到的FragmentManager則是另一個不同的物件,而另一個不同在於它則屬於FirstFragment自身。
(P.S:關於HostCallBack這個東西,有興趣的朋友可以自己研究原始碼或者關於Fragment原始碼分析的文章。簡單的來說,它是屬於FragmentActivity的內部類,getSupportFragmentManager實際就是通過控制HostCallback返回FragmentManagerImpl物件)

好的,那麼現在由此其實不難想象,既然Fragment會多出這麼一個特定的方法,肯定是有其存在的意義的。
現在假定我們依然使用FragmentMananger在FirstFragment中管理側滑選單的子碎片,那麼首先可能會出現如下所示的問題:

這裡可以看到的一個問題就是:當我們在底部導航由第一頁轉至第三頁後,第一頁的fragment中間的內容仍然沒有消失。
其實原因不難分析,因為前面說到如果此時使用getFragmentManager,意味著此時獲取到的FM物件其實和MainActivity中使用的FM是同一個物件。
也就是說,如此進行新增,側滑選單對應的三個Fragment其實仍然是被add進了與FirstFragment隸屬的相同的FragmentManager的集合內。

那麼,假定我們把側滑選單對應的第一個fragment物件命名為Child1Fragment,這其實也就意味著:
我們視覺上看上去屬於第一頁(即FirstFragment)的內容其實本來真正應該是屬於Child1Fragment的內容。
但由於我們通過getFragmentManager進行add操作,我們通過如下程式碼完成前面說到由第一頁跳轉至第三頁的操作則會導致:

ft.hide(first)
  .show(third)
  .commit;

雖然我們按照邏輯hide了FirstFragment物件,但關鍵在於:因為它們都屬於同一個FM物件,所以其實Child1Fragment仍然沒有被hide。

由此其實我們不難想象:如果通過FragmentManager在Fragment中巢狀Fragment,將由於邏輯的嚴重混亂,而造成難以管理
那麼,與之對應的,如果選擇使用getChildFragmentManager的好處有哪些呢?我們可以簡單的概括一下:

首先,最重要的一點:通過ChildFragmentManager進行管理的子Fragment物件,與其父Fragment物件的生命週期是息息相關的

舉個例子,假設我們用SecondFragment物件來replace掉FirstFragment物件,這時候有了前面的基礎,我們都知道:
FristFragment將走入onPause開始的這段生命週期。而使用ChildFragmentManager的好處在於,其內部的子Fragment也會受相同的生命週期管理。
顯然,我們可以預見由此帶來的最大的好處就是:此時各個Fragment之間的邏輯清晰,層級分佈明確,將大大利於我們對其進行管理

另一個非常實用的好處就在於:在這種管理模式下,子Fragment能夠很容易的實現與父Fragment之間進行通訊。通過一個例子能更形象的理解。
依舊是本文最初的圖例,我在FirstFragment中放置了一個ToolBar,那麼如果我切換了選項卡,想要在子Fragment的動態的操作Toolbar,就能這麼做:

    public void doSomething(){
        // do what you want to do
    }

是的,首先在FirstFragment中我們提供這一樣一個回撥方法,然後在子Fragment中我們就可以通過如下方式與其發生互動:

        FirstFragment parent = (FirstFragment) getParentFragment();
        parent.doSomething();

這都是一些非常實用的小技巧,更多的延伸實用,我們可以自己在實際中拓展。總之:如果是剛開始接觸Fragment,一定記住:
如果是在Fragment中新增Fragment時,請一定記住選擇通過getChildFragmentManager來對碎片進行管理

那麼,現在我們言歸正傳。通過ChildFragmentManager是不是就能解決我們說到的監聽fragment隱藏/顯示了呢?
其實不難推測出答案。我們再次回到那個情景,在第三頁清楚快取後,回到第一頁,那麼很明顯符合我們邏輯的實現就是:

ft.hide(third)
  .show(first)
  .commit;

現在我相信我們都很清楚了,這時候通過onHiddenChanged肯定是能夠監聽到FirstFragment的,但對於巢狀在其內的Child1Fragment就不行了。
但是因為之前的基礎,這個問題顯然已不難解決。我們可以在FirstFragment中定義一個index來記錄此時的側滑選單的子Fragment索引,隨後:

    // FirstFragment.java
    @Override
    public void onHiddenChanged(boolean hidden) {
        getChildFragmentManager().getFragments().get(currentIndex).onHiddenChanged(hidden);
    }

怎麼樣,是不是很簡單呢?這就是我們本文中提到的問題的第二種場景延伸。所以說細節真的很能夠幫助我們更加靈活的掌握一件事物。

與ViewPager的配合

現在我們進一步深入,正如本文圖例所示,Child1Fragment中有一個ViewPager,ViewPager中有放置了兩個Fragment。
再次衍生我們之前的場景描述:現在在“第三頁”清除快取過後,再次回到第一頁,此時第一頁顯示的正好是ViewPager中的碎片。
那麼,這個時候我們應該如何監聽對應的這個位於ViewPager中的Fragment物件呢?相信有了之前的基礎,我們很容易類推出來。
既然上一節中我們已經將顯示狀態的改變由FirstFragment傳遞到了Child1Fragment,那麼現在只需要繼續向ViewPager進行傳遞就行了。

    // Child1Fragment.java
    @Override
    public void onHiddenChanged(boolean hidden) {
       getChildFragmentManager().getFragments().get(mViewPager.getCurrentItem()).onHiddenChanged(hidden);
    }

這便是我們本文描述的問題的第三種場景延伸。但是關於fragment配合viewpager使用時,依然還有很多值得留意的小技巧和細節。

ViewPager的快取機制

通過之前的分析與總結,我們已經清楚通過FragmentManager管理碎片時,Fragment的生命週期變化情況。
那麼,當Fragment配合ViewPager時,Fragment的生命週期又是什麼情況呢?我們還是可以自己驗證一下。
為了能得到更準確的結論,可以把Child1Fragment中ViewPager放置的Fragment數量新增到4個,再通過切換來檢視各個碎片的生命週期。

為了節省篇幅,我們選擇檢視兩個最具有代表性的生命週期變化的片段截圖,首先是ViewPager的初始顯示時的片段截圖:

可以看到雖然ViewPager初始時,只需要顯示第一個Fragment,但是第二個Fragment物件仍然經過了初始化的生命週期。接著:
假設我們進一步的操作是直接將ViewPager由第一個Fragment滑動至第三個,然後我們再來瞧一瞧對應的生命週期變化:

由此我們可以發現,這個時候不僅切換到的第三個Fragment進行了初始化,與它相鄰的第四個碎片同樣也進行了初始化。與此同時,可以發現:
在這之前顯示的第一個Fragment則經過了onPause到onDestoryView的生命週期變化,也就是說這時第一個Fragment的檢視會被銷燬。

這其實就是ViewPager自身的一個快取機制,預設情況下它會幫我們快取一個Fragment相鄰的兩個Fragment物件。簡單來說,就像上面表現的:
當第一個Fragment需要顯示時,其相鄰的第二個物件也會進行初始化。第三個Fragment需要顯示時,左邊第二個物件已經完成了初始化,於是右邊的第四個則會進行初始化。我們不難推測出設計者如此設計的初衷:顯然這是為了使用者對ViewPager有更好的體驗,設想一下:

  • 當用戶進入到ViewPager的第一個檢視,這時相鄰的第二個檢視也已經進行了初始化。那麼當用戶切換到第二頁,則可以直接進行瀏覽了。
  • 當用戶切換到第三頁的時候,之所以選擇銷燬掉第一頁的檢視,則是為了減少嵌入的Fragment數量,減少滑動時出現卡頓的可能性。

setOffscreenPageLimit

瞭解了ViewPager的快取機制,則有一個比較實用的東西叫做setOffscreenPageLimit,它的作用就是來設定這個快取的上限。
這個上限的預設值為1,而當該值為1時,其效果就和我們上一節描述的一樣。我們可以自己設定該值來改變這個快取的數量。
但與此同時,需要注意的另一個細節是,要避免做出類似如下程式碼所示的這種想當然的操作:

mViewPager.setOffscreenPageLimit(0);

這行程式碼是無法完成你本來想要實現的目的的,究其原因,可以在原始碼中找到答案:

從程式碼中不難看到,當我們傳入的limit引數小於DEFAULT_OFFSCREEN_PAGES時,就將直接被設定為等同於這個預設值。
那麼DEFAULT_OFFSCREEN_PAGES的值究竟是多少呢?其實從前面的分析就能得出結論,其值為1:

setUserVisibleHint

我們也許已經留意到,在前面的生命週期變化中,有一個叫做setUserVisibleHint的東西反覆出現了不少次。其實有了之前的基礎,就容易理解了。
之前通過FragmentManager控制碎片的隱藏和顯示,回撥的是onHiddenChanged方法。而在ViewPager則沒有通過FM來進行控制。
所以不難推測,這時Fragment物件的顯示和隱藏,回撥多半就不是onHiddenChanged了。事實正是如此,此時的回撥則是setUserVisibleHint。
所以說,如果想要在這種情況監聽Fragment物件的隱藏/顯示,那麼監聽這個方法就可以了。這也理解為我們本文提出的問題的第四種場景延伸。

最後,這裡注意一下這種情景與我們之前描述的第三種場景的區別。之前說到的對於ViewPager中的碎片的顯示/隱藏狀態監聽的解決方案,是從針對從其它Fragment切換到ViewPager中的某個Fragment顯示的情景。而監聽setUserVisibleHint則是針對於都是位於ViewPager內的Fragment物件相互之前的切換顯示的情況。

ViewPager的懶載入

其實寫到這裡,對於我能想到的關於本文最初說到的那個朋友提出的問題 常見的情景延伸都已經總結到了,本想結束。
但是前面說到ViewPager的快取機制時,我們提到ViewPager會根據設定的快取數量上限來控制相應數量的Fragment物件提前初始化。
這就可能涉及到另一個比較實用的小技巧:配合ViewPager時,Fragment的懶載入。雖然網上該類資料很多,但還是可以簡單總結一下。

所謂的懶載入其實很好理解,我們說了正常情況下,ViewPager會對某指定數量的Fragment進行預初始化。
而通常在Fragment初始化的生命週期裡:我們都會做一些與該Fragment相關的資料的載入工作等等。那麼:
在一些時候,比如某個Fragment在初始化時需要載入的資料量較大;或者說因為資料來源於網路等原因,
此時等待該Fragment完成初始化,就會從一定程度上影響到應用的效率,這個時候就產生了所謂的“懶載入”的需求。
其實懶載入的本質非常簡單:那就是不要在ViewPager預初始化的時候去載入資料,而是當該Fragment真正顯示時才進行載入。

因為我們之前已經知道了setUserVisibleHint這個東西,所以其實解決方案就不難給出了。這裡可以給出一個簡單的模板,僅供參考:

public abstract class LazyLoadFragment extends Fragment{

    protected boolean isPrepared;
    protected boolean isLoadedOnce;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {
        View view = inflater.inflate(getLayoutId(), container, false);

        isPrepared = true;
        // 資料載入
        loadData();

        return view;
    }

    protected abstract int getLayoutId() ;

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        loadData();
    }

    protected void loadData() {
        if(!isPrepared || !getUserVisibleHint() || isLoadedOnce)
            return;
        // 懶載入
        lazyLoad();
        isLoadedOnce = true;
    }

    protected abstract void lazyLoad();

    @Override
    public void onDetach() {
        super.onDetach();
        isPrepared = isLoadedOnce = false;
    }

}

上面的程式碼應該不難理解,如果需要實現懶載入,則可以讓Fragment繼承該類,然後再覆寫用於載入資料的lazyLoad方法就可以了。