以前處理 Fragment 的懶加載,我們通常會在 Fragment 中處理 setUserVisibleHint + onHiddenChanged
這兩個函數,而在 Androidx 模式下,我們可以使用 FragmentTransaction.setMaxLifecycle()
的方式來處理 Fragment 的懶加載。
在本文章中,我會詳細介紹不同使用場景下兩種方案的差異。大家快拿好小板凳。一起來學習新知識吧!
本篇文章涉及到的 Demo,已上傳至Github---->傳送門
老的懶加載處理方案
如果你熟悉老一套的 Fragment 懶加載機制,你可以直接查看 Androix 懶加載相關章節
add+show+hide 模式下的老方案
在沒有添加懶加載之前,只要使用 add+show+hide
的方式控制並顯示 Fragment, 那么不管 Fragment 是否嵌套,在初始化后,如果只調用了add+show
,同級下的 Fragment 的相關生命周期函數都會被調用。且調用的生命周期函數如下所示:
onAttach -> onCreate -> onCreatedView -> onActivityCreated -> onStart -> onResume
Fragment 完整生命周期:onAttach -> onCreate -> onCreatedView -> onActivityCreated -> onStart -> onResume -> onPause -> onStop -> onDestroyView -> onDestroy -> onDetach
什么是同級 Frament 呢?看下圖

上圖中,都是使用
add+show+hide
的方式控制 Fragment,
在上圖兩種模式中:
- Fragment_1、Fragment_2、Fragment_3 屬於同級 Fragment
- Fragment_a、Fragment_b、Fragment_c 屬於同級 Fragment
- Fragment_d、Fragment_e、Fragment_f 屬於同級 Fragment
那這種方式會帶來什么問題呢?結合下圖我們來分別分析。

觀察上圖我們可以發現,同級的Fragment_1、Fragment_2、Fragment_3 都調用了 onAttach...onResume
系列方法,也就是說,如果我們沒有對 Fragment 進行懶加載處理,那么我們就會無緣無故的加載一些並不可見
的 Fragment , 也就會造成用戶流量的無故消耗(我們會在 Fragment 相關生命周期函數中,請求網絡或其他數據操作)。
這里
"不可見的Fragment"
是指,實際不可見但是相關可見生命周期函數(如onResume
方法)被調用的 Fragment
如果使用嵌套 Fragment ,這種浪費流量的行為就更明顯了。以本節的圖一為例,當 Fragment_1 加載時,如果你在 Fragment_1 生命周期函數中使用 show+add+hide
的方式添加 Fragment_a、Fragment_b、Fragment_c
, 那么 Fragment_b 又會在其生命周期函數中繼續加載 Fragment_d、Fragment_e、Fragment_f
。
那如何解決這種問題呢?我們繼續接着上面的例子走,當我們 show Fragment_2
,並 hide其他 Fragment 時,對應 Fragment 的生命周期調用如下:

從上圖中,我們可以看出 Fragment_2 與 Fragment_3 都調用了 onHiddenChanged
函數,該函數的官方 API 聲明如下:
/** * Called when the hidden state (as returned by {@link #isHidden()} of * the fragment has changed. Fragments start out not hidden; this will * be called whenever the fragment changes state from that. * @param hidden True if the fragment is now hidden, false otherwise. */ public void onHiddenChanged(boolean hidden) { }
根據官方 API 的注釋,我們大概能知道,當 Fragment 隱藏的狀態發生改變時,該函數將會被調用,如果當前 Fragment 隱藏, hidden
的值為 true, 反之為 false。最為重要的是hidden
的值,可以通過調用 isHidden()
函數獲取。
那么結合上述知識點,我們能推導出:
- 因為 Fragment_1 的
隱藏狀態
從可見轉為了不可見
,所以其onHiddenChanged
函數被調用,同時hidden
的值為 true。 - 同理對於 Fragment_2 ,因為其
隱藏狀態
從不可見轉為了可見
,所以其 hidden 值為 false。 - 對於 Fragment_3 ,因為其隱藏狀態從始至終都沒有發生變化,所以其 onHiddenChanged 函數並不會調用。
嗯,好像有點眉目了。不急,我們繼續看下面的例子。
show Fragment_3 並 hide 其他 Fragment ,對應生命周期函數調用如下所示:

從圖中,我們可以看出,確實只有隱藏狀態
發生了改變的 Fragment 其 onHiddenChanged
函數才會調用,那么結合以上知識點,我們能得出如下重要結論:
只要通過 show+hide
方式控制 Fragment 的顯隱,那么在第一次初始化后,Fragment 任何的生命周期方法都不會調用,只有 onHiddenChanged
方法會被調用。
那么,假如我們要在 add+show+hide
模式下控制 Fragment 的懶加載,我們只需要做這兩步:
- 我們需要在
onResume()
函數中調用isHidden()
函數,來處理默認顯示的 Fragment - 在
onHiddenChanged
函數中控制其他不可見的Fragment,
也就是這樣處理:
abstract class LazyFragment:Fragment(){ private var isLoaded = false //控制是否執行懶加載 override fun onResume() { super.onResume() judgeLazyInit() } override fun onHiddenChanged(hidden: Boolean) { super.onHiddenChanged(hidden) isVisibleToUser = !hidden judgeLazyInit() } private fun judgeLazyInit() { if (!isLoaded && !isHidden) { lazyInit() isLoaded = true } } override fun onDestroyView() { super.onDestroyView() isLoaded = false } //懶加載方法 abstract fun lazyInit() }
該懶加載的實現,是在
onResume
方法中操作,當然你可以在其他生命周期函數中控制。但是建議在該方法中執行懶加載。
ViewPager+Fragment 模式下的老方案
使用傳統方式處理 ViewPager 中 Fragment 的懶加載,我們需要控制 setUserVisibleHint(boolean isVisibleToUser)
函數,該函數的聲明如下所示:
public void setUserVisibleHint(boolean isVisibleToUser) {}
該函數與之前我們介紹的 onHiddenChanged()
作用非常相似,都是通過傳入的參數值來判斷當前 Fragment 是否對用戶可見,只是 onHiddenChanged()
是在 add+show+hide
模式下使用,而 setUserVisibleHint
是在 ViewPager+Fragment 模式下使用。
在本節中,我們用 FragmentPagerAdapter + ViewPager
為例,向大家講解如何實現 Fragment 的懶加載。
注意:在本例中沒有調用
setOffscreenPageLimit
方法去設置 ViewPager 預緩存的 Fragment 個數。默認情況下 ViewPager 預緩存 Fragment 的個數為1
。
初始化 ViewPager 查看內部 Fragment 生命周期函數調用情況:

觀察上圖,我們能發現 ViePager 初始化時,默認會調用其內部 Fragment 的 setUserVisibleHint 方法,因為其預緩存 Fragment 個數為 1
的原因,所以只有 Fragment_1 與 Fragment_2 的生命周期函數被調用。
我們繼續切換到 Fragment_2,查看各個Fragment的生命周期函數的調用變化。如下圖所示:

觀察上圖,我們同樣發現 Fragment 的 setUserVisibleHint 方法被調用了,並且 Fragment_3 的一系列生命周期函數被調用了。繼續切換到 Fragment_3:

觀察上圖可以發現,Fragment_3 調用了 setUserVisibleHint 方法,繼續又切換到 Fragment_1,查看調用函數的變化:

因為之前在切換到 Fragment_3 時,Frafgment_1 已經走了 onDestoryView(圖二,藍色標記處) 方法,所以 Fragment_1 需要重新走一次生命周期。
那么結合本節的三幅圖,我們能得出以下結論:
- 使用 ViewPager,切換回上一個 Fragment 頁面時(已經初始化完畢),不會回調任何生命周期方法以及onHiddenChanged(),只有 setUserVisibleHint(boolean isVisibleToUser) 會被回調。
- setUserVisibleHint(boolean isVisibleToUser) 方法總是會優先於 Fragment 生命周期函數的調用。
所以如果我們想對 ViewPager 中的 Fragment 懶加載,我們需要這樣處理:
abstract class LazyFragment : Fragment() { /** * 是否執行懶加載 */ private var isLoaded = false /** * 當前Fragment是否對用戶可見 */ private var isVisibleToUser = false /** * 當使用ViewPager+Fragment形式會調用該方法時,setUserVisibleHint會優先Fragment生命周期函數調用, * 所以這個時候就,會導致在setUserVisibleHint方法執行時就執行了懶加載, * 而不是在onResume方法實際調用的時候執行懶加載。所以需要這個變量 */ private var isCallResume = false override fun onResume() { super.onResume() isCallResume = true judgeLazyInit() } private fun judgeLazyInit() { if (!isLoaded && isVisibleToUser && isCallResume) { lazyInit() Log.d(TAG, "lazyInit:!!!!!!!”) isLoaded = true } } override fun onHiddenChanged(hidden: Boolean) { super.onHiddenChanged(hidden) isVisibleToUser = !hidden judgeLazyInit() } //在Fragment銷毀View的時候,重置狀態 override fun onDestroyView() { super.onDestroyView() isLoaded = false isVisibleToUser = false isCallResume = false } override fun setUserVisibleHint(isVisibleToUser: Boolean) { super.setUserVisibleHint(isVisibleToUser) this.isVisibleToUser = isVisibleToUser judgeLazyInit() } abstract fun lazyInit() }
復雜 Fragment 嵌套的情況
當然,在實際項目中,我們可能會遇到更為復雜的 Fragment 嵌套組合。比如 Fragment+Fragment、Fragment+ViewPager、ViewPager+ViewPager….等等。
如下圖所示:

對於以上場景,我們就需要重寫我們的懶加載,以支持不同嵌套組合模式下 Fragment 正確懶加載。我們需要將 LazyFragment 修改成如下這樣:
abstract class LazyFragment : Fragment() { /** * 是否執行懶加載 */ private var isLoaded = false /** * 當前Fragment是否對用戶可見 */ private var isVisibleToUser = false /** * 當使用ViewPager+Fragment形式會調用該方法時,setUserVisibleHint會優先Fragment生命周期函數調用, * 所以這個時候就,會導致在setUserVisibleHint方法執行時就執行了懶加載, * 而不是在onResume方法實際調用的時候執行懶加載。所以需要這個變量 */ private var isCallResume = false /** * 是否調用了setUserVisibleHint方法。處理show+add+hide模式下,默認可見 Fragment 不調用 * onHiddenChanged 方法,進而不執行懶加載方法的問題。 */ private var isCallUserVisibleHint = false override fun onResume() { super.onResume() isCallResume = true if (!isCallUserVisibleHint) isVisibleToUser = !isHidden judgeLazyInit() } private fun judgeLazyInit() { if (!isLoaded && isVisibleToUser && isCallResume) { lazyInit() Log.d(TAG, "lazyInit:!!!!!!!”) isLoaded = true } } override fun onHiddenChanged(hidden: Boolean) { super.onHiddenChanged(hidden) isVisibleToUser = !hidden judgeLazyInit() } override fun onDestroyView() { super.onDestroyView() isLoaded = false isVisibleToUser = false isCallUserVisibleHint = false isCallResume = false } override fun setUserVisibleHint(isVisibleToUser: Boolean) { super.setUserVisibleHint(isVisibleToUser) this.isVisibleToUser = isVisibleToUser isCallUserVisibleHint = true judgeLazyInit() } abstract fun lazyInit() }
Androidx 下的懶加載
雖然之前的方案就能解決輕松的解決 Fragment 的懶加載,但這套方案有一個最大的弊端,就是不可見的 Fragment 執行了 onResume() 方法
。onResume 方法設計的初衷,難道不是當前 Fragment 可以和用戶進行交互嗎?你他媽既不可見,又不能和用戶進行交互,你執行 onResume 方法干嘛?
基於此問題,Google 在 Androidx 在 FragmentTransaction
中增加了 setMaxLifecycle
方法來控制 Fragment 所能調用的最大的生命周期函數。如下所示:
/** * Set a ceiling for the state of an active fragment in this FragmentManager. If fragment is * already above the received state, it will be forced down to the correct state. * * <p>The fragment provided must currently be added to the FragmentManager to have it’s * Lifecycle state capped, or previously added as part of this transaction. The * {@link Lifecycle.State} passed in must at least be {@link Lifecycle.State#CREATED}, otherwise * an {@link IllegalArgumentException} will be thrown.</p> * * @param fragment the fragment to have it's state capped. * @param state the ceiling state for the fragment. * @return the same FragmentTransaction instance */
根據官方的注釋,我們能知道,該方法可以設置活躍狀態下 Fragment 最大的狀態,如果該 Fragment 超過了設置的最大狀態,那么會強制將 Fragment 降級到正確的狀態。
那如何使用該方法呢?我們先看該方法在 Androidx 模式下 ViewPager+Fragment 模式下的使用例子。
ViewPager+Fragment 模式下的方案
在 FragmentPagerAdapter 與 FragmentStatePagerAdapter 新增了含有 behavior
字段的構造函數,如下所示:
public FragmentPagerAdapter(@NonNull FragmentManager fm, @Behavior int behavior) { mFragmentManager = fm; mBehavior = behavior; } public FragmentStatePagerAdapter(@NonNull FragmentManager fm, @Behavior int behavior) { mFragmentManager = fm; mBehavior = behavior; }
其中 Behavior 的聲明如下:
@Retention(RetentionPolicy.SOURCE) @IntDef({BEHAVIOR_SET_USER_VISIBLE_HINT, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT}) private @interface Behavior { } /** * Indicates that {@link Fragment#setUserVisibleHint(boolean)} will be called when the current * fragment changes. * * @deprecated This behavior relies on the deprecated * {@link Fragment#setUserVisibleHint(boolean)} API. Use * {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT} to switch to its replacement, * {@link FragmentTransaction#setMaxLifecycle}. * @see #FragmentPagerAdapter(FragmentManager, int) */ @Deprecated public static final int BEHAVIOR_SET_USER_VISIBLE_HINT = 0; /** * Indicates that only the current fragment will be in the {@link Lifecycle.State#RESUMED} * state. All other Fragments are capped at {@link Lifecycle.State#STARTED}. * * @see #FragmentPagerAdapter(FragmentManager, int) */ public static final int BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT = 1;
從官方的注釋聲明中,我們能得到如下兩條結論:
- 如果 behavior 的值為
BEHAVIOR_SET_USER_VISIBLE_HINT
,那么當 Fragment 對用戶的可見狀態發生改變時,setUserVisibleHint
方法會被調用。 - 如果 behavior 的值為
BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT
,那么當前選中的 Fragment 在Lifecycle.State#RESUMED
狀態 ,其他不可見的 Fragment 會被限制在Lifecycle.State#STARTED
狀態。
那 BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT 這個值到底有什么作用呢?我們看下面的例子:
在該例子中設置了 ViewPager 的適配器為 FragmentPagerAdapter 且 behavior 值為
BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT
。
默認初始化ViewPager,Fragment 生命周期如下所示:

切換到 Fragment_2 時,日志情況如下所示:

切換到 Fragment_3 時,日志情況如下所示:

因為篇幅的原因,本文沒有在講解 FragmentStatePagerAdapter 設置 behavior 下的使用情況,但是原理以及生命周期函數調用情況一樣,感興趣的小伙伴,可以根據 AndroidxLazyLoad 項目自行測試。
觀察上述例子,我們可以發現,使用了 BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT
后,確實只有當前可見的 Fragment 調用了 onResume 方法。而導致產生這種改變的原因,是因為 FragmentPagerAdapter 在其 setPrimaryItem
方法中調用了 setMaxLifecycle
方法,如下所示:
public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) { Fragment fragment = (Fragment)object; //如果當前的fragment不是當前選中並可見的Fragment,那么就會調用 // setMaxLifecycle 設置其最大生命周期為 Lifecycle.State.STARTED if (fragment != mCurrentPrimaryItem) { if (mCurrentPrimaryItem != null) { mCurrentPrimaryItem.setMenuVisibility(false); if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { if (mCurTransaction == null) { mCurTransaction = mFragmentManager.beginTransaction(); } mCurTransaction.setMaxLifecycle(mCurrentPrimaryItem, Lifecycle.State.STARTED); } else { mCurrentPrimaryItem.setUserVisibleHint(false); } } //對於其他非可見的Fragment,則設置其最大生命周期為 //Lifecycle.State.RESUMED fragment.setMenuVisibility(true); if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { if (mCurTransaction == null) { mCurTransaction = mFragmentManager.beginTransaction(); } mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED); } else { fragment.setUserVisibleHint(true); } mCurrentPrimaryItem = fragment; } }
既然在上述條件下,只有實際可見的 Fragment 會調用 onResume 方法, 那是不是為我們提供了 ViewPager 下實現懶加載的新思路呢?也就是我們可以這樣實現 Fragment 的懶加載:
abstract class LazyFragment : Fragment() { private var isLoaded = false override fun onResume() { super.onResume() if (!isLoaded) { lazyInit() Log.d(TAG, "lazyInit:!!!!!!!”) isLoaded = true } } override fun onDestroyView() { super.onDestroyView() isLoaded = false } abstract fun lazyInit() }
add+show+hide 模式下的新方案
雖然我們實現了Androidx 包下 ViewPager下的懶加載,但是我們仍然要考慮 add+show+hide 模式下的 Fragment 懶加載的情況,基於 ViewPager 在 setPrimaryItem
方法中的思路,我們可以在調用 add+show+hide 時,這樣處理:
完整的代碼請點擊--->ShowHideExt
/** * 使用add+show+hide模式加載fragment * * 默認顯示位置[showPosition]的Fragment,最大Lifecycle為Lifecycle.State.RESUMED * 其他隱藏的Fragment,最大Lifecycle為Lifecycle.State.STARTED * *@param containerViewId 容器id *@param showPosition fragments *@param fragmentManager FragmentManager *@param fragments 控制顯示的Fragments */ private fun loadFragmentsTransaction( @IdRes containerViewId: Int, showPosition: Int, fragmentManager: FragmentManager, vararg fragments: Fragment ) { if (fragments.isNotEmpty()) { fragmentManager.beginTransaction().apply { for (index in fragments.indices) { val fragment = fragments[index] add(containerViewId, fragment, fragment.javaClass.name) if (showPosition == index) { setMaxLifecycle(fragment, Lifecycle.State.RESUMED) } else { hide(fragment) setMaxLifecycle(fragment, Lifecycle.State.STARTED) } } }.commit() } else { throw IllegalStateException( "fragments must not empty” ) } } /** 顯示需要顯示的Fragment[showFragment],並設置其最大Lifecycle為Lifecycle.State.RESUMED。 * 同時隱藏其他Fragment,並設置最大Lifecycle為Lifecycle.State.STARTED * @param fragmentManager * @param showFragment */ private fun showHideFragmentTransaction(fragmentManager: FragmentManager, showFragment: Fragment) { fragmentManager.beginTransaction().apply { show(showFragment) setMaxLifecycle(showFragment, Lifecycle.State.RESUMED) //獲取其中所有的fragment,其他的fragment進行隱藏 val fragments = fragmentManager.fragments for (fragment in fragments) { if (fragment != showFragment) { hide(fragment) setMaxLifecycle(fragment, Lifecycle.State.STARTED) } } }.commit() }
上述代碼的實現也非常簡單:
- 將需要顯示的 Fragment ,在調用 add 或 show 方法后,
setMaxLifecycle(showFragment, Lifecycle.State.RESUMED)
- 將需要隱藏的 Fragment ,在調用 hide 方法后,
setMaxLifecycle(fragment, Lifecycle.State.STARTED)
結合上述操作模式,查看使用 setMaxLifecycle 后,Fragment 生命周期函數調用的情況。
add Fragment_1、Fragment_2、Fragment_3,並 hide Fragment_2,Fragment_3
:

show Fragment_2,hide 其他 Fragment:

show Fragment_3 hide 其他 Fragment:

參考上圖,好像真的也能處理懶加載!!!!!美滋滋
並不完美的 setMaxLifecycle
當我第一次使用 setMaxLifycycle 方法時,我也和大家一樣覺得萬事大吉。但這套方案仍然有點點瑕疵,當 Fragment 的嵌套時,即使使用了 setMaxLifycycle 方法,第一次初始化時,同級不可見的Fragment,仍然 TMD 要調用可見生命周期方法。看下面的例子:

不知道是否是谷歌大大沒有考慮到 Fragment 嵌套的情況,所以這里我們要對之前的方案就行修改,也就是如下所示:
abstract class LazyFragment : Fragment() { private var isLoaded = false override fun onResume() { super.onResume() //增加了Fragment是否可見的判斷 if (!isLoaded && !isHidden) { lazyInit() Log.d(TAG, "lazyInit:!!!!!!!”) isLoaded = true } } override fun onDestroyView() { super.onDestroyView() isLoaded = false } abstract fun lazyInit() }
在上述代碼中,因為同級的 Fragment 在嵌套模式下,仍然要調用 onResume 方法,所以我們增加了 Fragment 可見性的判斷,這樣就能保證嵌套模式下,新方案也能完美的支持 Fragment 的懶加載。
ViewPager2 的處理方案
ViewPager2 本身就支持對實際可見的 Fragment 才調用 onResume 方法。關於 ViewPager2 的內部機制。感興趣的小伙伴可以自行查看源碼。
關於 ViewPager2 的懶加載測試,已上傳至 AndroidxLazyLoad,大家可以結合項目查看Log日志。
兩種方式的對比與總結
老一套的懶加載
- 優點:不用去控制 FragmentManager的 add+show+hide 方法,所有的懶加載都是在Fragment 內部控制,也就是控制
setUserVisibleHint + onHiddenChanged
這兩個函數。 - 缺點:實際不可見的 Fragment,其
onResume
方法任然會被調用,這種反常規的邏輯,無法容忍。
新一套的懶加載(Androidx下setMaxLifecycle)
- 優點:
在非特殊的情況下(缺點1)
,只有實際的可見 Fragment,其onResume
方法才會被調用,這樣才符合方法設計的初衷。 - 缺點:
- 對於 Fragment 的嵌套,及時使用了
setMaxLifecycle
方法。同級不可見的Fragment, 仍然要調用onResume
方法。 - 需要在原有的 add+show+hide 方法中,繼續調用 setMaxLifecycle 方法來控制Fragment 的最大生命狀態。
- 對於 Fragment 的嵌套,及時使用了
最后
這兩種方案的優缺點已經非常明顯了,到底該選擇何種懶加載模式,還是要基於大家的意願,作者我更傾向於使用新的方案。關於 Fragment 的懶加載實現,非常願意聽到大家不同的聲音,如果你有更好的方案,可以在評論區留下您的 idea,期待您的回復。
推薦閱讀:一線互聯網企業100萬年薪面試題大全(含答案解析)
推薦關注:Android面試專題NO.1
作者:AndyJennifer
鏈接:https://www.jianshu.com/p/2201a107d5b5