年後最後一篇文章啦,在這裏先祝你們新年快樂~最重要的抽中
全家福
,明年繼續修福報🤣java
之前處理 Fragment 的懶加載,咱們一般會在 Fragment 中處理 setUserVisibleHint + onHiddenChanged
這兩個函數,而在 Androidx 模式下,咱們可使用 FragmentTransaction.setMaxLifecycle()
的方式來處理 Fragment 的懶加載。android
在本文章中,我會詳細介紹不一樣使用場景下兩種方案的差別。你們快拿好小板凳。一塊兒來學習新知識吧!git
本篇文章涉及到的 Demo,已上傳至Github---->傳送門github
若是你熟悉老一套的 Fragment 懶加載機制,你能夠直接查看 Androix 懶加載相關章節緩存
在沒有添加懶加載以前,只要使用 add+show+hide
的方式控制並顯示 Fragment, 那麼無論 Fragment 是否嵌套,在初始化後,若是只調用了add+show
,同級下的 Fragment 的相關生命週期函數都會被調用。且調用的生命週期函數以下所示:網絡
onAttach -> onCreate -> onCreatedView -> onActivityCreated -> onStart -> onResume
app
Fragment 完整生命週期:onAttach -> onCreate -> onCreatedView -> onActivityCreated -> onStart -> onResume -> onPause -> onStop -> onDestroyView -> onDestroy -> onDetachide
什麼是同級 Frament 呢?看下圖函數
上圖中,都是使用
add+show+hide
的方式控制 Fragment,學習
在上圖兩種模式中:
那這種方式會帶來什麼問題呢?結合下圖咱們來分別分析。
觀察上圖咱們能夠發現,同級的Fragment_一、Fragment_二、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()
函數獲取。
那麼結合上述知識點,咱們能推導出:
隱藏狀態
從可見轉爲了避免可見
,因此其 onHiddenChanged
函數被調用,同時 hidden
的值爲 true。隱藏狀態
從 不可見轉爲了可見
,因此其 hidden 值爲 false。嗯,好像有點眉目了。不急,咱們繼續看下面的例子。
show Fragment_3 並 hide 其餘 Fragment ,對應生命週期函數調用以下所示:
從圖中,咱們能夠看出,確實只有隱藏狀態
發生了改變的 Fragment 其 onHiddenChanged
函數纔會調用,那麼結合以上知識點,咱們能得出以下重要結論:
只要經過 show+hide
方式控制 Fragment 的顯隱,那麼在第一次初始化後,Fragment 任何的生命週期方法都不會調用,只有 onHiddenChanged
方法會被調用。
那麼,假如咱們要在 add+show+hide
模式下控制 Fragment 的懶加載,咱們只須要作這兩步:
onResume()
函數中調用 isHidden()
函數,來處理默認顯示的 FragmentonHiddenChanged
函數中控制其餘不可見的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 的懶加載,咱們須要控制 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 懶加載,咱們須要這樣處理:
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+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()
}
複製代碼
雖然以前的方案就能解決輕鬆的解決 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 */
@NonNull
public FragmentTransaction setMaxLifecycle(@NonNull Fragment fragment, @NonNull Lifecycle.State state) {
addOp(new Op(OP_SET_MAX_LIFECYCLE, fragment, state));
return this;
}
複製代碼
根據官方的註釋,咱們能知道,該方法能夠設置活躍狀態下 Fragment 最大的狀態,若是該 Fragment 超過了設置的最大狀態,那麼會強制將 Fragment 降級到正確的狀態。
那如何使用該方法呢?咱們先看該方法在 Androidx 模式下 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_SET_USER_VISIBLE_HINT
,那麼當 Fragment 對用戶的可見狀態發生改變時,setUserVisibleHint
方法會被調用。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()
}
複製代碼
雖然咱們實現了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()
}
複製代碼
上述代碼的實現也很是簡單:
setMaxLifecycle(showFragment, Lifecycle.State.RESUMED)
setMaxLifecycle(fragment, Lifecycle.State.STARTED)
結合上述操做模式,查看使用 setMaxLifecycle 後,Fragment 生命週期函數調用的狀況。
add Fragment_一、Fragment_二、Fragment_3,並 hide Fragment_2,Fragment_3 :
show Fragment_2,hide 其餘 Fragment:
show Fragment_3 hide 其餘 Fragment:
參考上圖,好像真的也能處理懶加載!!!!!美滋滋
當我第一次使用 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 自己就支持對實際可見的 Fragment 才調用 onResume 方法。關於 ViewPager2 的內部機制。感興趣的小夥伴能夠自行查看源碼。
關於 ViewPager2 的懶加載測試,已上傳至 AndroidxLazyLoad,你們能夠結合項目查看Log日誌。
setUserVisibleHint + onHiddenChanged
這兩個函數。onResume
方法任然會被調用,這種反常規的邏輯,沒法容忍。在非特殊的狀況下(缺點1)
,只有實際的可見 Fragment,其 onResume
方法纔會被調用,這樣才符合方法設計的初衷。setMaxLifecycle
方法。同級不可見的Fragment, 仍然要調用 onResume
方法。這兩種方案的優缺點已經很是明顯了,到底該選擇何種懶加載模式,仍是要基於你們的意願,做者我更傾向於使用新的方案。關於 Fragment 的懶加載實現,很是願意聽到你們不一樣的聲音,若是你有更好的方案,能夠在評論區留下您的 idea,期待您的回覆。若是您以爲本篇文章對你有所幫助,請不要吝嗇你的關注與點贊。ღ( ´・ᴗ・` )比心