多層巢狀後的 Fragment 懶載入實現

像一隻狗發表於2019-03-04

多層巢狀後的 Fragment 懶載入實現

多層巢狀後的 Fragment 懶載入

印象中從 Feed 流應用流行開始,Fragment 懶載入變成了一個大家都需要關注的開發知識,關於 Fragment 的懶載入,網上有很多例子,GitHub 上也有很多例子,就連我自己在一年前也寫過相關的文章。但是之前的應用可能最多的是一層 Activity + ViewPager 的 UI 層次,但是隨著頁面越來越複雜,越來越多的應用首頁一個頁面外層是一個 ViewPager 內部可能還巢狀著一層 ViewPager,這是之前的懶載入就可能不那麼好用了。本文對於多層 ViewPager 的巢狀使用過程中,Fragment 主要的三個狀態:第一次可見,每次可見,每次不可見,提供解決方案。

為什麼要使用懶載入

在我們開發中經常會使用 ViewPager + Fragment 來建立多 tab 的頁面,此時在 ViewPager 內部預設會幫我們快取當頁面前後兩個頁面的 Fragment 內容,如果使用了 setOffscreenPageLimit 方法,那麼 ViewPager 初始化的時候將會快取對應引數個 Fragment。為了增加使用者體驗我們往往會使用該方法來保證載入過的頁面不被銷燬,並留離開 tab 之前的狀態(列表滑動距離等),而我們在使用 Fragment 的時候往往在建立完 View 後,就會開始網路請求等操作,如果存在上述的需求時,懶載入就顯得尤為重要了,不僅可以節省使用者流量,還可以在提高應用效能,給使用者帶來更加的體驗。

ViewPager + Fragment 的懶載入實質上我們就在做三件事,就可以將上邊所說的效果實現,那麼就是找到每個 Fragment 第一對使用者可見的時機,和每次 Fragment 對使用者可見時機,以及每次 Framgment 對使用者不可見的時機,來暴露給實現實現類做對應的網路請求或者網路請求中斷時機。下面我們就來從常見的幾種 UI 結構上一步步實現無論巢狀多少層,無論開發者使用的 hide show 還是 ViewPager 巢狀都能準確獲取這三種狀態的時機的一種懶載入實現方案。

單層 ViewPager + Fragment 懶載入

我們都知道 Fragment 生命週期按先後順序有

onAttach -> onCreate -> onCreatedView -> onActivityCreated -> onStart -> onResume -> onPause -> onStop -> onDestroyView -> onDestroy -> onDetach

對於 ViewPager + Fragment 的實現我們需要關注的幾個生命週期有:

onCreatedView + onActivityCreated + onResume + onPause + onDestroyView

以及非生命週期函式:

setUserVisibleHint + onHiddenChanged

對於單層 ViewPager + Fragment 可能是我們最常用的頁面結構了,如網易雲音樂的首頁頂部的是三個 tab ,我們那網易雲音樂作為例子:

多層巢狀後的 Fragment 懶載入實現

對於這種 ViewPager + Fragment 結構,我們使用的過程中一般只包含是 3 種情況分別是:

  1. 使用 FragmentPagerAdapterFragmentPagerStateAdapter不設定 setOffscreenPageLimit

    • 左右滑動頁面,每次只快取下一個 Pager ,和上一個 Pager
    • 間隔的點選 tab 如從位於 tab1 的時候直接選擇 tab3 或 tab4 ,tab1將會被銷燬
  2. 使用 FragmentPagerAdapterFragmentPagerStateAdapter 設定 setOffscreenPageLimit 為 tab 總數

    • 建立 ViewPager 的時候所有頁面都將建立完成,生命週期走到 onResume
    • 間隔的點選 tab 如從位於 tab1 的時候直接選擇 tab3 或 tab4, tab1不會被銷燬
  3. 進入其他頁面或者使用者按 home 鍵回到桌面,當前 ViewPager 頁面變成不見狀態。

對於 FragmentPagerAdapterFragmentPagerStateAdapter 的區別在於在於,前者在 Fragment 不見的時候將不會 detach ,而後者將會銷燬 Fragmentdetach 掉。

實際上這也是所有 ViewPager 的操作情況。

  • 第一種情況不設定 setOffscreenPageLimit 左右滑動頁面/或者每次選擇相鄰 tab 的情況 FragmentPagerAdapterFragmentPagerStateAdapter 有所區別
 BottomTabFragment1  setUserVisibleHint false
 BottomTabFragment2  setUserVisibleHint false
 BottomTabFragment1  setUserVisibleHint true
 
 BottomTabFragment1  onCreateView
 BottomTabFragment1  onActivityCreated 
 BottomTabFragment1  onResume  

 BottomTabFragment2  onCreateView
 BottomTabFragment2  onActivityCreated 
 BottomTabFragment2  onResume   
 
 //滑動到 Tab 2
 BottomTabFragment3  setUserVisibleHint false
 BottomTabFragment1  setUserVisibleHint false
 BottomTabFragment2  setUserVisibleHint true

 BottomTabFragment3  onCreateView
 BottomTabFragment3  onActivityCreated 
 BottomTabFragment3 onResume  
 
 //跳過 Tab3 直接選擇 Tab4
 BottomTabFragment4  setUserVisibleHint false
 BottomTabFragment2  setUserVisibleHint false
 BottomTabFragment4  setUserVisibleHint true
 
 BottomTabFragment4  onCreateView
 BottomTabFragment4  onActivityCreated 
 BottomTabFragment4  onResume   
 
 BottomTabFragment2  onPause 
 BottomTabFragment2  onDestroyView
 
 // FragmentPagerStateAdapter 會走一下兩個生命週期方法
 BottomTabFragment2  onDestroy 
 BottomTabFragment2  onDetach  

 BottomTabFragment1  onPause 
 BottomTabFragment1  onDestroyView
 
 // FragmentPagerStateAdapter 會走一下兩個生命週期方法
 BottomTabFragment1  onDestroy 
 BottomTabFragment1  onDetach 
 
 // 使用者回到桌面 再回到當前 APP 開啟其他頁面當前頁面的生命週期也是這樣的
 BottomTabFragment3  onPause 
 BottomTabFragment4  onPause 
 BottomTabFragment3  onStop 
 BottomTabFragment4  onStop 
 
 BottomTabFragment3 onResume  
 BottomTabFragment4 onResume  
 

複製程式碼
  • 第二種情況設定 setOffscreenPageLimit 為 Pager 的個數時候,左右滑動頁面/或者每次選擇相鄰 tab 的情況 FragmentPagerAdapter 和 FragmentPagerStateAdapter 沒有區別
  BottomTabFragment1  setUserVisibleHint false
  BottomTabFragment2  setUserVisibleHint false
  BottomTabFragment3  setUserVisibleHint false
  BottomTabFragment4  setUserVisibleHint false

  BottomTabFragment1  setUserVisibleHint true
  BottomTabFragment1  onCreateView
  BottomTabFragment1  onActivityCreated 
  BottomTabFragment1 onResume  

  BottomTabFragment2  onCreateView
  BottomTabFragment2  onActivityCreated 

  BottomTabFragment3  onCreateView
  BottomTabFragment3  onActivityCreated 

  BottomTabFragment4  onCreateView
  BottomTabFragment4  onActivityCreated 
  
  BottomTabFragment2 onResume  
  BottomTabFragment3 onResume  
  BottomTabFragment4 onResume 
  
  //選擇 Tab2
  BottomTabFragment1  setUserVisibleHint false
  BottomTabFragment2  setUserVisibleHint true

 //跳過 Tab3 直接選擇 Tab4
  BottomTabFragment2  setUserVisibleHint false
  BottomTabFragment4  setUserVisibleHint true
  
  // 使用者回到桌面 再回到當前 APP 開啟其他頁面當前頁面的生命週期也是這樣的
 BottomTabFragment1  onPause 
 BottomTabFragment2  onPause 
 BottomTabFragment3  onPause 
 BottomTabFragment4  onPause  
 
 BottomTabFragment1 onResume  
 BottomTabFragment2 onResume  
 BottomTabFragment3 onResume  
 BottomTabFragment4 onResume
   
複製程式碼

可以看出第一次執行 setUserVisibleHint(boolean isVisibleToUser) 除了可見的 Fragment 外都為 false,還可以看出除了這一點不同以外,所有的 Fragment 都走到了生命週期 onResume 階段。而選擇相鄰 tab 的時候已經初始化完成的Fragment 並不再重新走生命週期方法,只是 setUserVisibleHint(boolean isVisibleToUser) 為 true。當使用者進入其他頁面的時候所有 ViewPager 快取的 Fragment 都會呼叫 onPause 生命週期函式,當再次回到當前頁面的時候都會呼叫 onResume。

能發現這一點,其實對於單層 ViewPager 巢狀 Fragment 可見狀態的把握其實已經很明顯了。下面給出我的解決方案:

  1. 對於 Fragment 可見狀態的判斷需要設定兩個標誌位 ,Fragment View 建立完成的標誌位 isViewCreatedFragment 第一次建立的標誌位 mIsFirstVisible

  2. 為了獲得 Fragment 不可見的狀態,和再次回到可見狀態的判斷,我們還需要增加一個 currentVisibleState 標誌位,該標誌位在 onResume 中和 onPause 中結合 getUserVisibleHint 的返回值來決定是否應該回撥可見和不可見狀態函式。

整個可見過程判斷邏輯如下圖所示

多層巢狀後的 Fragment 懶載入實現

多層巢狀後的 Fragment 懶載入實現

接下來我們就來看下具體實現:

public abstract class LazyLoadBaseFragment extends BaseLifeCircleFragment {

    protected View rootView = null;


    private boolean mIsFirstVisible = true;

    private boolean isViewCreated = false;

    private boolean currentVisibleState = false;


    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        //走這裡分發可見狀態情況有兩種,1. 已快取的 Fragment 被展示的時候 2. 當前 Fragment 由可見變成不可見的狀態時
        // 對於預設 tab 和 間隔 checked tab 需要等到 isViewCreated = true 後才可以通過此通知使用者可見,
        // 這種情況下第一次可見不是在這裡通知 因為 isViewCreated = false 成立,可見狀態在 onActivityCreated 中分發
        // 對於非預設 tab,View 建立完成  isViewCreated =  true 成立,走這裡分發可見狀態,mIsFirstVisible 此時還為 false  所以第一次可見狀態也將通過這裡分發
        if (isViewCreated){
            if (isVisibleToUser && !currentVisibleState) {
                dispatchUserVisibleHint(true);
            }else if (!isVisibleToUser && currentVisibleState){
                dispatchUserVisibleHint(false);
            }
        }
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        // 將 View 建立完成標誌位設為 true
        isViewCreated = true;
        // 預設 Tab getUserVisibleHint() = true !isHidden() = true
        // 對於非預設 tab 或者非預設顯示的 Fragment 在該生命週期中只做了 isViewCreated 標誌位設定 可見狀態將不會在這裡分發
        if (!isHidden() && getUserVisibleHint()){
            dispatchUserVisibleHint(true);
        }

    }


    /**
     * 統一處理 顯示隱藏  做兩件事
     * 設定當前 Fragment 可見狀態 負責在對應的狀態呼叫第一次可見和可見狀態,不可見狀態函式 
     * @param visible
     */
    private void dispatchUserVisibleHint(boolean visible) {

        currentVisibleState = visible;

        if (visible) {
            if (mIsFirstVisible) {
                mIsFirstVisible = false;
                onFragmentFirstVisible();
            }
            onFragmentResume();
        }else {
            onFragmentPause();
        }
    }

    /**
     * 該方法與 setUserVisibleHint 對應,呼叫時機是 show,hide 控制 Fragment 隱藏的時候,
     * 注意的是,只有當 Fragment 被建立後再次隱藏顯示的時候才會呼叫,第一次 show 的時候是不會回撥的。
     */
    @Override
    public void onHiddenChanged(boolean hidden) {
        super.onHiddenChanged(hidden);
        if (hidden){
            dispatchUserVisibleHint(false);
        }else {
            dispatchUserVisibleHint(true);
        }
    }

    /**
     * 需要再 onResume 中通知使用者可見狀態的情況是在當前頁面再次可見的狀態 !mIsFirstVisible 可以保證這一點,
     * 而當前頁面 Activity 可見時所有快取的 Fragment 都會回撥 onResume 
     * 所以我們需要區分那個Fragment 位於可見狀態 
     * (!isHidden() && !currentVisibleState && getUserVisibleHint())可條件可以判定哪個 Fragment 位於可見狀態
     */
    @Override
    public void onResume() {
        super.onResume();
        if (!mIsFirstVisible){
            if (!isHidden() && !currentVisibleState && getUserVisibleHint()){
                dispatchUserVisibleHint(true);
            }
        }
    }

    /** 
     * 當使用者進入其他介面的時候所有的快取的 Fragment 都會 onPause
     * 但是我們想要知道只是當前可見的的 Fragment 不可見狀態,
     * currentVisibleState && getUserVisibleHint() 能夠限定是當前可見的 Fragment
     */
    @Override
    public void onPause() {
        super.onPause();

        if (currentVisibleState && getUserVisibleHint()){
            dispatchUserVisibleHint(false);
        }
    }
    
    
    @Override
    public void onDestroyView() {
        super.onDestroyView();
        //當 View 被銷燬的時候我們需要重新設定 isViewCreated mIsFirstVisible 的狀態
        isViewCreated = false;
        mIsFirstVisible = true;
    }

    /**
     * 對使用者第一次可見
     */
    public void onFragmentFirstVisible(){
        LogUtils.e(getClass().getSimpleName() + "  ");
    }
    
    /**
     *   對使用者可見
     */
    public void onFragmentResume(){
        LogUtils.e(getClass().getSimpleName() + "  對使用者可見");
    }
    
   /**
     *  對使用者不可見
     */
    public void onFragmentPause(){
        LogUtils.e(getClass().getSimpleName() + "  對使用者不可見");
    }


    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        super.onCreateView(inflater,container,savedInstanceState);

        if (rootView == null) {
            rootView = inflater.inflate(getLayoutRes(), container, false);
        }

        initView(rootView);

        return rootView;
    }

    /**
     * 返回佈局 resId
     *
     * @return layoutId
     */
    protected abstract int getLayoutRes();


    /**
     * 初始化view
     *
     * @param rootView
     */
    protected abstract void initView(View rootView);
}
複製程式碼

我們使之前的 Fragment 改為繼承 LazyLoadBaseFragment 列印 log 可以看出:

//預設選中第一 Tab
 BottomTabFragment1  setUserVisibleHint false
 BottomTabFragment2  setUserVisibleHint false
 BottomTabFragment3  setUserVisibleHint false
 BottomTabFragment4  setUserVisibleHint false
 BottomTabFragment1  setUserVisibleHint true

 BottomTabFragment1  onCreateView
 BottomTabFragment1  onActivityCreated 
 BottomTabFragment1  對使用者第一次可見
 BottomTabFragment1  對使用者可見
 BottomTabFragment1  onResume  

 BottomTabFragment2  onCreateView
 BottomTabFragment2  onActivityCreated 

 BottomTabFragment3  onCreateView
 BottomTabFragment3  onActivityCreated 

 BottomTabFragment4  onCreateView
 BottomTabFragment4  onActivityCreated 

 BottomTabFragment2 onResume  
 BottomTabFragment3 onResume  
 BottomTabFragment4 onResume  

 //滑動選中 Tab2
 BottomTabFragment1  setUserVisibleHint false
 BottomTabFragment1  對使用者不可見

 BottomTabFragment2  setUserVisibleHint true
 BottomTabFragment2  對使用者第一次可見
 BottomTabFragment2  對使用者可見

 //間隔選中 Tab4
 BottomTabFragment2  setUserVisibleHint false
 BottomTabFragment2  對使用者不可見

 BottomTabFragment4  setUserVisibleHint true
 BottomTabFragment4  對使用者第一次可見
 BottomTabFragment4  對使用者可見


 // 回退到桌面
 BottomTabFragment1  onPause 
 BottomTabFragment2  onPause 
 BottomTabFragment3  onPause 
 BottomTabFragment4  onPause 
 BottomTabFragment4  對使用者不可見

 BottomTabFragment1  onStop 
 BottomTabFragment2  onStop 
 BottomTabFragment3  onStop 
 BottomTabFragment4  onStop 

 // 再次進入 APP
 BottomTabFragment1 onResume  
 BottomTabFragment2 onResume  
 BottomTabFragment3 onResume  
 BottomTabFragment4 onResume  
 BottomTabFragment4  對使用者可見
複製程式碼

上述 log 只演示瞭如何 ViewPager 中的函式列印,由於 hide show 方法顯示隱藏的 Fragment 有人可能認為不需要懶載入這個東西,如果說從建立來說的確是這樣的,但是如果說所有的 Fragment 已經 add 進 Activity 中,此時 Activity 退到後臺,所有的 Fragment 都會呼叫 onPause ,並且在其進入前臺的前臺統一會回撥 onResume, 如果我們在 Resume 中做了某些操作,那麼不可見的 Fragment 也會執行,勢必也是個浪費。所以這裡的懶載入吧 hide show 的展示方法也考慮進去。

對於無巢狀的 ViewPager ,懶載入還是相對簡單的。但是對於ViewPager 巢狀 ViewPager 的情況可能就出現一些我們意料不到的情況。

雙層 ViewPager 巢狀的懶載入實現

對於雙層 ViewPager 巢狀我們也拿網易雲來舉例:

多層巢狀後的 Fragment 懶載入實現

可以看出頂層的第二 tab 內部又是一個 ViewPager ,那麼我們試著按照我們之前的方案列印一下生命週期過程:

 BottomTabFragment1  setUserVisibleHint false
 BottomTabFragment2  setUserVisibleHint false
 
 BottomTabFragment1  setUserVisibleHint true
 BottomTabFragment1  onCreateView
 BottomTabFragment1  onActivityCreated 
 BottomTabFragment1  對使用者第一次可見
 BottomTabFragment1  對使用者可見

 BottomTabFragment1 onResume  
 BottomTabFragment2  onCreateView
 BottomTabFragment2  onActivityCreated 
 BottomTabFragment2 onResume  

 Bottom2InnerFragment1  setUserVisibleHint false
 Bottom2InnerFragment2  setUserVisibleHint false
 Bottom2InnerFragment1  setUserVisibleHint true
 
 //注意這裡 位於第二個Tab 中的 ViewPager 中的第一個 Tab 也走了可見,而它本身並不可見
 Bottom2InnerFragment1  onCreateView
 Bottom2InnerFragment1  onActivityCreated 
 Bottom2InnerFragment1  對使用者第一次可見
 Bottom2InnerFragment1  對使用者可見
 Bottom2InnerFragment1  onResume  

 Bottom2InnerFragment2  onCreateView
 Bottom2InnerFragment2  onActivityCreated 
 Bottom2InnerFragment2 onResume  
複製程式碼

咦奇怪的事情發生了,對於外層 ViewPager 的第二個 tab 預設是不顯示的,為什麼內部 ViewPager 中的 Bottom2InnerFragment1 卻走了可見了狀態回撥。是不是 onActivityCreated 中的寫法有問題,!isHidden() && getUserVisibleHint() getUserVisibleHint() 方法通過 log 列印發現在 Bottom2InnerFragment1 onActivityCreated 時候, Bottom2InnerFragment1 setUserVisibleHint true的確是 true。所以才會走到分發可見事件中。

我們再回頭看下上述的生命週期的列印,可以發現,事實上作為父 Fragment 的 BottomTabFragment2 並沒有分發可見事件,他通過 getUserVisibleHint() 得到的結果為 false,首先我想到的是能在負責分發事件的方法中判斷一下當前父 fragment 是否可見,如果父 fragment 不可見我們就不進行可見事件的分發,我們試著修改 dispatchUserVisibleHint 如下面所示:

  private void dispatchUserVisibleHint(boolean visible) {
        //當前 Fragment 是 child 時候 作為快取 Fragment 的子 fragment getUserVisibleHint = true
        //但當父 fragment 不可見所以 currentVisibleState = false 直接 returnif (visible && isParentInvisible()) return;
        
        currentVisibleState = visible;

        if (visible) {
            if (mIsFirstVisible) {
                mIsFirstVisible = false;
                onFragmentFirstVisible();
            }
            onFragmentResume();
        } else {
            onFragmentPause();
        }
    }
    
 /**
  * 用於分發可見時間的時候父獲取 fragment 是否隱藏
  * @return true fragment 不可見, false 父 fragment 可見
  */
 private boolean isParentInvisible() {
    LazyLoadBaseFragment fragment = (LazyLoadBaseFragment) getParentFragment();
    return fragment != null && !fragment.isSupportVisible();
   
 }
 
private boolean isSupportVisible() {
    return currentVisibleState;
}
複製程式碼

通過日誌列印我們發現這似乎起作用了:

 BottomTabFragment1  setUserVisibleHint false
 BottomTabFragment2  setUserVisibleHint false
 
 BottomTabFragment1  setUserVisibleHint true
 BottomTabFragment1  onCreateView
 BottomTabFragment1  onActivityCreated 
 BottomTabFragment1  對使用者第一次可見
 BottomTabFragment1  對使用者可見

 BottomTabFragment1 onResume  
 BottomTabFragment2  onCreateView
 BottomTabFragment2  onActivityCreated 
 BottomTabFragment2 onResume  

 Bottom2InnerFragment1  setUserVisibleHint false
 Bottom2InnerFragment2  setUserVisibleHint false
 Bottom2InnerFragment1  setUserVisibleHint true
 
 Bottom2InnerFragment1  onCreateView
 Bottom2InnerFragment1  onActivityCreated 
 Bottom2InnerFragment1  onResume  

 Bottom2InnerFragment2  onCreateView
 Bottom2InnerFragment2  onActivityCreated 
 Bottom2InnerFragment2 onResume  
 
 //滑動到第二個 tab
 BottomTabFragment3  setUserVisibleHint false
 BottomTabFragment1  setUserVisibleHint false
 BottomTabFragment1  對使用者不可見
 
 BottomTabFragment2  setUserVisibleHint true
 BottomTabFragment2  對使用者第一次可見
 BottomTabFragment2  對使用者可見
 
 BottomTabFragment3  onCreateView
 BottomTabFragment3  onActivityCreated 
 BottomTabFragment3 onResume  
複製程式碼

但是我們又發現了新的問題,當我們滑動到第二個 tab 時候,無疑我們期望得到第二個 tab 中內層 ViewPager 第一個 tab 中 fragment 狀態的可見狀態,但是從上邊的 log 可以發現我們並沒有獲得其可見狀態的列印,問題出當外層 ViewPager 初始化的時候我們已經經歷了 Bottom2InnerFragment1 的初始化,而我們在 dispatchUserVisibleHint 做了攔截,導致其無法分發可見事件,當其真正可見的時候卻發現事件函式並不會再次被呼叫了。

本著堅信一切困難都是紙老虎的社會主義光榮理念,我404了一下,發現網上極少的巢狀 fragment 懶載入的文章中,大多都採用了,在父 Fragment 可見的時候,分發自己可見狀態的同時,把自己的可見狀態通知子 Fragment,對於可見狀態的 生命週期呼叫順序,父 Fragment總是優先於子 Fragment,所以我們在 Fragment 分發事件的時候,可以在上述攔截子 Fragment 事件分發後,當在父 Fragment 第一可見的時候,通知子 Fragment 你也可見了。所以我再次修改 dispatchUserVisibleHint,在父 Fragment 分發完成自己的可見事件後,讓子 Fragment 再次呼叫自己的可見事件分發方法,這次 isParentInvisible() 將會返回 false ,也就是可見狀態將會正確分發。

private void dispatchUserVisibleHint(boolean visible) {
   //當前 Fragment 是 child 時候 作為快取 Fragment 的子 fragment getUserVisibleHint = true
   //但當父 fragment 不可見所以 currentVisibleState = false 直接 returnif (visible && isParentInvisible()) return;
   
   currentVisibleState = visible;

   if (visible) {
       if (mIsFirstVisible) {
           mIsFirstVisible = false;
           onFragmentFirstVisible();
       }
       onFragmentResume();
       //可見狀態的時候內層 fragment 生命週期晚於外層 所以在 onFragmentResume 後分發
       dispatchChildVisibleState(true);
   } else {
       onFragmentPause();
       dispatchChildVisibleState(false);

   }
}

 private void dispatchChildVisibleState(boolean visible) {
       FragmentManager childFragmentManager = getChildFragmentManager();
       List<Fragment> fragments = childFragmentManager.getFragments();
       if (!fragments.isEmpty()) {
           for (Fragment child : fragments) {
               // 如果只有當前 子 fragment getUserVisibleHint() = true 時候分發事件,並將 也就是我們上面說的 Bottom2InnerFragment1
               if (child instanceof LazyLoadBaseFragment && !child.isHidden() && child.getUserVisibleHint()) {
                   ((LazyLoadBaseFragment) child).dispatchUserVisibleHint(visible);
               }
           }
       }
    }
複製程式碼

dispatchChildVisibleState 方法通過 childFragmentManager 獲取當前 Fragment 中所有的子 Fragment 並通過判斷 child.getUserVisibleHint() 的返回值,判斷是否應該通知子 Fragment 不可見,同理在父 Fragment 真正可見的時候,我們也會通過該方法,通知child.getUserVisibleHint() = true 的子 Fragment 你可見。

我們再次列印可以看出經過這次調整內層 Fragment 已經可以準確地拿到自己第一次可見狀態了。

   BottomTabFragment3  setUserVisibleHint false
   BottomTabFragment1  setUserVisibleHint false
   BottomTabFragment1  對使用者不可見
   
   BottomTabFragment2  setUserVisibleHint true
   BottomTabFragment2  對使用者第一次可見
   BottomTabFragment2  對使用者可見
   
   Bottom2InnerFragment1  對使用者第一次可見
   Bottom2InnerFragment1  對使用者可見
   
   BottomTabFragment3  onCreateView
   BottomTabFragment3  onActivityCreated  
   BottomTabFragment3 onResume  
複製程式碼

當我以為紙老虎一進被我大打敗的時候,我按了下 home 鍵看了條微信,然後發現 log 列印如下:

 BottomTabFragment1  onPause 
 
 //Bottom2InnerFragment1 第一不可見回撥
 Bottom2InnerFragment1  onPause 
 Bottom2InnerFragment1  對使用者不可見
 
 Bottom2InnerFragment2  onPause 
 BottomTabFragment2  onPause 
 
 BottomTabFragment2  對使用者不可見
 //Bottom2InnerFragment1 第二次不可見回撥
 Bottom2InnerFragment1  對使用者不可見
 BottomTabFragment3  onPause 
 BottomTabFragment1  onStop 

 Bottom2InnerFragment1  onStop 
 Bottom2InnerFragment2  onStop 

 BottomTabFragment2  onStop 
 BottomTabFragment3  onStop  
複製程式碼

這又是啥情況? 為啥回撥了兩次,我連微信都忘了回就開始回憶之前分發可見事件的程式碼,可見的時候時候沒問題,為什麼不可見會回撥兩次?後來發現問題出現在事件分發的順序上。

通過日誌列印我們也可以看出,對於可見狀態的生命週期呼叫順序,父 Fragment總是優先於子 Fragment,而對於不可見事件,內部的 Fragment 生命週期總是先於外層 Fragment。所以第一的時候 Bottom2InnerFragment1 呼叫自身的 dispatchUserVisibleHint 方法分發了不可見事件,作為父 Fragment 的BottomTabFragment2 分發不可見的時候,又會再次呼叫 dispatchChildVisibleState ,導致子 Fragment 再次呼叫自己的 dispatchUserVisibleHint 再次呼叫了一次 onFragmentPause();

解決辦法也很簡單,還記得 currentVisibleState 這個變數麼? 表示當前 Fragment 的可見狀態,如果當前的 Fragment 要分發的狀態與 currentVisibleState 相同我們就沒有必要去做分發了。

我們知道子 Fragment 優於父 Fragment回撥本方法 currentVisibleState 置位 false,當前不可見,我們可以當父 dispatchChildVisibleState 的時候第二次回撥本方法 visible = false 所以此處 visible 將直接返回。

private void dispatchUserVisibleHint(boolean visible) {

   if (visible && isParentInvisible()) 
     return;
     
    // 此處是對子 Fragment 不可見的限制,因為 子 Fragment 先於父 Fragment回撥本方法 currentVisibleState 置位 false
   // 當父 dispatchChildVisibleState 的時候第二次回撥本方法 visible = false 所以此處 visible 將直接返回
   if (currentVisibleState == visible) {
       return;
   }
        
   currentVisibleState = visible;

   if (visible) {
       if (mIsFirstVisible) {
           mIsFirstVisible = false;
           onFragmentFirstVisible();
       }
       onFragmentResume();
       //可見狀態的時候內層 fragment 生命週期晚於外層 所以在 onFragmentResume 後分發
       dispatchChildVisibleState(true);
   } else {
       onFragmentPause();
       dispatchChildVisibleState(false);
   }
}
複製程式碼

對於 Hide And show 方法顯示的 Fragment 驗證這裡講不在過多贅述,上文也說了,對這種 Fragment 展示方法,我們更需要關注的是 hide 的時候, onPause 和 onResume 再次隱藏顯示的的時候。改方法的驗證可以通過下載 Demo 檢視 log。Demo 地址

最終的實現方案

下面是完整 LazyLoadBaseFragment 實現方案:也可以直接戳此下載檔案 LazyLoadBaseFragment.java

import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import java.util.List;

/**
 * @author wangshijia
 * @date 2018/2/2
 * Fragment 第一次可見狀態應該在哪裡通知使用者 在 onResume 以後?
 */
public abstract class LazyLoadBaseFragment extends BaseLifeCircleFragment {

    protected View rootView = null;


    private boolean mIsFirstVisible = true;

    private boolean isViewCreated = false;

    private boolean currentVisibleState = false;

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        super.onCreateView(inflater, container, savedInstanceState);

        if (rootView == null) {
            rootView = inflater.inflate(getLayoutRes(), container, false);
        }
        initView(rootView);
        return rootView;
    }


    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        // 對於預設 tab 和 間隔 checked tab 需要等到 isViewCreated = true 後才可以通過此通知使用者可見
        // 這種情況下第一次可見不是在這裡通知 因為 isViewCreated = false 成立,等從別的介面回到這裡後會使用 onFragmentResume 通知可見
        // 對於非預設 tab mIsFirstVisible = true 會一直保持到選擇則這個 tab 的時候,因為在 onActivityCreated 會返回 false
        if (isViewCreated) {
            if (isVisibleToUser && !currentVisibleState) {
                dispatchUserVisibleHint(true);
            } else if (!isVisibleToUser && currentVisibleState) {
                dispatchUserVisibleHint(false);
            }
        }
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        isViewCreated = true;
        // !isHidden() 預設為 true  在呼叫 hide show 的時候可以使用
        if (!isHidden() && getUserVisibleHint()) {
            dispatchUserVisibleHint(true);
        }

    }

    @Override
    public void onHiddenChanged(boolean hidden) {
        super.onHiddenChanged(hidden);
        LogUtils.e(getClass().getSimpleName() + "  onHiddenChanged dispatchChildVisibleState  hidden " + hidden);

        if (hidden) {
            dispatchUserVisibleHint(false);
        } else {
            dispatchUserVisibleHint(true);
        }
    }

    @Override
    public void onResume() {
        super.onResume();
        if (!mIsFirstVisible) {
            if (!isHidden() && !currentVisibleState && getUserVisibleHint()) {
                dispatchUserVisibleHint(true);
            }
        }
    }

    @Override
    public void onPause() {
        super.onPause();
        // 當前 Fragment 包含子 Fragment 的時候 dispatchUserVisibleHint 內部本身就會通知子 Fragment 不可見
        // 子 fragment 走到這裡的時候自身又會呼叫一遍 ?
        if (currentVisibleState && getUserVisibleHint()) {
            dispatchUserVisibleHint(false);
        }
    }


    /**
     * 統一處理 顯示隱藏
     *
     * @param visible
     */
    private void dispatchUserVisibleHint(boolean visible) {
        //當前 Fragment 是 child 時候 作為快取 Fragment 的子 fragment getUserVisibleHint = true
        //但當父 fragment 不可見所以 currentVisibleState = false 直接 return 掉
        // 這裡限制則可以限制多層巢狀的時候子 Fragment 的分發
        if (visible && isParentInvisible()) return;

       //此處是對子 Fragment 不可見的限制,因為 子 Fragment 先於父 Fragment回撥本方法 currentVisibleState 置位 false
       // 當父 dispatchChildVisibleState 的時候第二次回撥本方法 visible = false 所以此處 visible 將直接返回
        if (currentVisibleState == visible) {
            return;
        }

        currentVisibleState = visible;

        if (visible) {
            if (mIsFirstVisible) {
                mIsFirstVisible = false;
                onFragmentFirstVisible();
            }
            onFragmentResume();
            dispatchChildVisibleState(true);
        } else {
            dispatchChildVisibleState(false);
            onFragmentPause();
        }
    }

    /**
     * 用於分發可見時間的時候父獲取 fragment 是否隱藏
     *
     * @return true fragment 不可見, false 父 fragment 可見
     */
    private boolean isParentInvisible() {
        Fragment parentFragment = getParentFragment();
        if (parentFragment instanceof LazyLoadBaseFragment ) {
            LazyLoadBaseFragment fragment = (LazyLoadBaseFragment) parentFragment;
            return !fragment.isSupportVisible();
        }else {
            return false;
        }
    }

    private boolean isSupportVisible() {
        return currentVisibleState;
    }

    /**
     * 當前 Fragment 是 child 時候 作為快取 Fragment 的子 fragment 的唯一或者巢狀 VP 的第一 fragment 時 getUserVisibleHint = true
     * 但是由於父 Fragment 還進入可見狀態所以自身也是不可見的, 這個方法可以存在是因為慶幸的是 父 fragment 的生命週期回撥總是先於子 Fragment
     * 所以在父 fragment 設定完成當前不可見狀態後,需要通知子 Fragment 我不可見,你也不可見,
     * <p>
     * 因為 dispatchUserVisibleHint 中判斷了 isParentInvisible 所以當 子 fragment 走到了 onActivityCreated 的時候直接 return 掉了
     * <p>
     * 當真正的外部 Fragment 可見的時候,走 setVisibleHint (VP 中)或者 onActivityCreated (hide show) 的時候
     * 從對應的生命週期入口呼叫 dispatchChildVisibleState 通知子 Fragment 可見狀態
     *
     * @param visible
     */
    private void dispatchChildVisibleState(boolean visible) {
        FragmentManager childFragmentManager = getChildFragmentManager();
        List<Fragment> fragments = childFragmentManager.getFragments();
        if (!fragments.isEmpty()) {
            for (Fragment child : fragments) {
                if (child instanceof LazyLoadBaseFragment && !child.isHidden() && child.getUserVisibleHint()) {
                    ((LazyLoadBaseFragment) child).dispatchUserVisibleHint(visible);
                }
            }
        }
    }

    public void onFragmentFirstVisible() {
        LogUtils.e(getClass().getSimpleName() + "  對使用者第一次可見");

    }

    public void onFragmentResume() {
        LogUtils.e(getClass().getSimpleName() + "  對使用者可見");
    }

    public void onFragmentPause() {
        LogUtils.e(getClass().getSimpleName() + "  對使用者不可見");
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        isViewCreated = false;
        mIsFirstVisible = true;
    }


    /**
     * 返回佈局 resId
     *
     * @return layoutId
     */
    protected abstract int getLayoutRes();


    /**
     * 初始化view
     *
     * @param rootView
     */
    protected abstract void initView(View rootView);
}
複製程式碼

總結

對於 ViewPager Fragment 懶載入網上文章可能已經很多了,但是對於多層 ViewPager + Fragment 巢狀的文章並不是很多,上文還原了我自己對 Fragment 懶載入的探索過程,目前該基類已經應用於公司專案中,相信隨著業務的複雜可能有的地方還有可能該方法存在缺陷,如果大家在使用過程中有問題也請給我留言。

最後感謝 YoKeyword 大神的Fragmentation 提供的思路,一個很好的單 Activity 多 Fragment 庫。

相關文章