判断当前界面是该fragment_学不动也要学!探究Fragment延迟加载的前世今生
碼個蛋(codeegg)?第?852?次推文
作者:一包純牛奶鏈接:https://juejin.im/post/5e085dafe51d45580769a1eb
碼妞看世界
大家普遍對于Fragment懶加載的問題比較關心,其實,對于Fragment懶加載問題的處理由來已久,網(wǎng)上不乏相關的優(yōu)秀文章。但是,由于Fragment生命周期的原因使得懶加載問題的處理并不是那么的優(yōu)雅。顯然,Google也意識到了問題所在。
因此,在Androidx的庫中對于Fragment的生命周期狀態(tài)的控制進行了深度優(yōu)化,使得我們更容易的去管控Fragment的生命周期,也使得我們更容易的去處理懶加載問題。但是,前提條件是我們要了解Google對于Fragment做了哪些優(yōu)化。那么就讓我們借此機會一起來探究一下吧!( 懶加載稱作延遲加載我覺得更貼切一些,所以下文就統(tǒng)稱為延遲加載了。 )
一、Fragment延遲加載的前世
雖然本篇文章是對于Fragment新特性的探究,但是我覺得寫文章總要有個因果。也為了照顧一下還不太了解什么是延遲加載的同學。我們還是先來了解一下延遲加載,順便回顧一下Fragment延遲加載的舊方案。
1. 為什么要對Fragment做延遲加載?
首先,我們要搞清楚一個問題。“Fragment延遲加載“中的“延遲”并不指的是延遲加載Fragment,而是延遲加載Fragment中的數(shù)據(jù)。對于Fragment的使用通常我們會結合ViewPager,ViewPager會默認在當前頁面的左右兩邊至少預加載一個頁面以保證ViewPager的流暢性。
我們假設在ViewPager的所有Fragment中都存在網(wǎng)絡請求,當我們打開這個頁面的時候由于ViewPager的預加載原因,即使在其它Fragment不可見的情況下也會去進行網(wǎng)絡請求加載數(shù)據(jù)。而如果此時用戶根本就沒有去滑動ViewPager就退出了應用或者切換到了其他頁面。那么對于這個不可見的Fragment中的網(wǎng)絡請求豈不是既浪費了流量也浪費了手機和服務器的性能?
那么此時有的同學就有問題了。你就不能在Fragment顯示的時候去加載數(shù)據(jù)嗎?
問的好!在解答之前我們先來看下Fragment的生命周期。
想必這張圖大家應該都非常熟悉了。
當Fragment被預加載的時候,此Fragment的生命周期會從onAttach執(zhí)行到onResume。顯然我們無法通過Fragment的生命周期來控制Fragment的延遲加載。
那么該怎么辦呢?我們且往下看。
2. 如何處理Fragment的延遲加載?
通過上一小節(jié)的分析我們知道想要在Fragment的生命周期中處理延遲加載的問題顯然是走不通的。所以想要處理Fragment的延遲加載就需要另想它法了。
還好,在Fragment中為我們提供了一個?setUserVisibleHint(isVisibleToUser: Boolean)?的方法,這個方法中有一個?isVisibleToUser?的boolean類型的參數(shù),其意義表示當前的Fragment是否對用戶可見。
因此,對于Fragment的延遲加載我們便可以通過這個方法來展開。既然要使用setUserVisibleHint(isVisibleToUser: Boolean)那么就應該知道這個方法的調(diào)用時機。我們寫一個ViewPager嵌套Fragment的例子來打印下日志:
注:上圖打印的日志中”position:0“表示當前Fragment,“position:1”表示預加載的Fragment,下同。
可見該方法是在Fragment的onAttach之前就已經(jīng)被調(diào)用了。因此,對于延遲加載我們可以在setUserVisibleHint(isVisibleToUser: Boolean)方法及onViewCreated(view: View, savedInstanceState: Bundle?)添加標志位來控制是否加載數(shù)據(jù)。我們來看下代碼:?
abstract class BaseLazyFragment : Fragment() { /** * 當前Fragment狀態(tài)是否可見 */ private var isVisibleToUser: Boolean = false /** * 是否已創(chuàng)建View */ private var isViewCreated: Boolean = false /** * 是否第一次加載數(shù)據(jù) */ private var isFirstLoad = true override fun setUserVisibleHint(isVisibleToUser: Boolean) { super.setUserVisibleHint(isVisibleToUser) this.isVisibleToUser = isVisibleToUser onLazyLoad() } private fun onLazyLoad() { if (isVisibleToUser && isViewCreated && isFirstLoad) { isFirstLoad = false lazyLoad() } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) isViewCreated = true onLazyLoad() }????protected?abstract?fun?lazyLoad()}我們通過在Fragment中添加了三個標志位實現(xiàn)了延遲加載的功能。我們到TestFragment嘗試一下:
class?TestFragment?:?BaseLazyFragment()?{ private var position: Int = 0 override fun setUserVisibleHint(isVisibleToUser: Boolean) { super.setUserVisibleHint(isVisibleToUser) val bundle = arguments position = bundle!!.getInt(KEY_POSITION) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val cardView = CardView(inflater, container) cardView.bind(Card.fromBundle(arguments!!),position) return cardView.view } companion object { private const val KEY_POSITION = "position" fun getInstance(card: Card, position: Int): TestFragment { val fragment = TestFragment() val bundle = card.toBundle() bundle.putInt(KEY_POSITION, position) fragment.arguments = bundle return fragment } } override fun lazyLoad() { showToast("Fragment$position is loading data") } private fun showToast(content: String) { Toast.makeText(context, content, Toast.LENGTH_SHORT).show()????}}我們來看下效果:
嗯!立竿見影,只有當Fragment完全顯示出來的時候loading data的操作才被執(zhí)行。這種延遲加載的方案在Androidx 1.1.0版本以前被廣泛應用。而在Androidx 1.1.0版本中,Google對于Fragment進行了優(yōu)化處理,使得延遲加載也有了新的解決方案。
二、Fragment的setMaxLifecycle探究
上一節(jié)中我們講到因為ViewPager的預加載機制以及Fragment的生命周期無法得以控制,我們不得不通過 setUserVisibleHint(isVisibleToUser: Boolean) 和 onViewCreated(view: View, savedInstanceState: Bundle?) 方法以及添加三個標志位來處理延遲加載,顯然這樣的代碼并不優(yōu)雅。
當我們將Android項目遷移到Androidx 并將 Androidx 版本升級到1.1.0之后發(fā)現(xiàn),我們第一節(jié)中用到的setUserVisibleHint(isVisibleToUser: Boolean)方法已被標記為廢棄了!
/** * ... 省略其它注釋 * @deprecated Use {@link FragmentTransaction#setMaxLifecycle(Fragment, Lifecycle.State)} * instead. */@Deprecatedpublic void setUserVisibleHint(boolean isVisibleToUser) { if (!mUserVisibleHint && isVisibleToUser && mState < STARTED && mFragmentManager != null && isAdded() && mIsCreated) { mFragmentManager.performPendingDeferredStart(this); } mUserVisibleHint = isVisibleToUser; mDeferStart = mState < STARTED && !isVisibleToUser; if (mSavedFragmentState != null) { // Ensure that if the user visible hint is set before the Fragment has // restored its state that we don't lose the new value mSavedUserVisibleHint = isVisibleToUser; }}并且從注釋中可以看到使用 FragmentTransaction#setMaxLifecycle(Fragment, Lifecycle.State)?方法來替換setUserVisibleHint方法。setMaxLifecycle實在Androidx 1.1.0中新增加的一個方法。
setMaxLifecycle從名字上來看意思是設置一個最大的生命周期,因為這個方法是在 FragmentTransaction 中,因此我們可以知道應該是為Fragment來設置一個最大的生命周期。我們來看下setMaxLifecycle的源碼:
/** * 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. * *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. * * @param fragment the fragment to have it's state capped. * @param state the ceiling state for the fragment. * @return the same FragmentTransaction instance */@NonNullpublic FragmentTransaction setMaxLifecycle(@NonNull Fragment fragment, @NonNull Lifecycle.State state) { addOp(new Op(OP_SET_MAX_LIFECYCLE, fragment, state)); return this;}這個方法接收一個Fragment參數(shù)和一個Lifecycle的狀態(tài)參數(shù)。Lifecycle是Jetpack中很重要的一個庫,它具有對Activity和Fragment生命周期感知能力,相信很多同學都應該對Lifecycle都略知一二。
在Lifecycle的State中定義了五種生命周期狀態(tài),如下:
public enum State { /** * Destroyed state for a LifecycleOwner. After this event, this Lifecycle will not dispatch * any more events. For instance, for an {@link android.app.Activity}, this state is reached * right before Activity's {@link android.app.Activity#onDestroy() onDestroy} call. */ DESTROYED, /** * Initialized state for a LifecycleOwner. For an {@link android.app.Activity}, this is * the state when it is constructed but has not received * {@link android.app.Activity#onCreate(android.os.Bundle) onCreate} yet. */ INITIALIZED, /** * Created state for a LifecycleOwner. For an {@link android.app.Activity}, this state * is reached in two cases: * * after {@link android.app.Activity#onCreate(android.os.Bundle) onCreate} call; * right before {@link android.app.Activity#onStop() onStop} call. * */ CREATED, /** * Started state for a LifecycleOwner. For an {@link android.app.Activity}, this state * is reached in two cases: * * after {@link android.app.Activity#onStart() onStart} call; * right before {@link android.app.Activity#onPause() onPause} call. * */ STARTED, /** * Resumed state for a LifecycleOwner. For an {@link android.app.Activity}, this state * is reached after {@link android.app.Activity#onResume() onResume} is called. */ RESUMED; /** * Compares if this State is greater or equal to the given {@code state}. * * @param state State to compare with * @return true if this State is greater or equal to the given {@code state} */ public boolean isAtLeast(@NonNull State state) { return compareTo(state) >= 0; } }而在?setMaxLifecycle?中接收的生命周期狀態(tài)要求不能低于?CREATED,否則會拋出一個?IllegalArgumentException?的異常。當傳入?yún)?shù)為DESTROYED或者INITIALIZED時則會拋出如下圖的異常:
因此除去這兩個生命周期外,僅剩下CREATED、STARTED、RESUMED三個生命周期狀態(tài)的參數(shù)可用,那么接下來我們就逐個來研究這三個參數(shù)的效果。
1. 不設置setMaxLifecycle
我們先來看下在不設置setMaxLifecycle的時候添加一個Fragment的狀態(tài),以便和后邊的情況進行對比。首先我們在Activity中添加一個Fragment,代碼如下:
fragment = TestLifecycleFragment.getInstance(Card.DECK[0], 0)val fragmentTransaction = supportFragmentManager.beginTransaction()fragmentTransaction.add(R.id.ll_fragment, fragment)fragmentTransaction.commit()啟動Activity,我們將該Fragment生命周期的日志打印出來如下:
可以看到這個Fragment生命周期從onAttach一直執(zhí)行到了onResume,?并且在Activity中成功顯示出了Fragment
2. setMaxLifecycle與CREATED
接下來,我們將maxLifecycle設置為CREATED:
fragment = TestLifecycleFragment.getInstance(Card.DECK[0], 0)val fragmentTransaction = supportFragmentManager.beginTransaction()fragmentTransaction.add(R.id.ll_fragment, fragment)fragmentTransaction.setMaxLifecycle(fragment, Lifecycle.State.CREATED)fragmentTransaction.commit()再來看日志輸出:
可以看到該Fragment的生命周期僅僅執(zhí)行到了onCreate就沒再往下執(zhí)行了。并且Activity中沒有加載出來當前Fragment。
那么現(xiàn)在問題來了,假設Fragment已經(jīng)執(zhí)行到了onResume, 此時再為Fragment設置一個CREATED的最大生命周期會出現(xiàn)什么樣的情況呢?我們通過日志來驗證一下:
從日志中可以看到已經(jīng)執(zhí)行了onResume的Fragment,將其最大生命周期設置為CREATED后會執(zhí)行onPause->onStop->onDestoryView。也就是回退到了onCreate的狀態(tài)。
3. setMaxLifecycle與STARTED
接下來,我們將maxLifecycle設置為STARTED:
fragment = TestLifecycleFragment.getInstance(Card.DECK[0], 0)val fragmentTransaction = supportFragmentManager.beginTransaction()fragmentTransaction.add(R.id.ll_fragment, fragment)fragmentTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED)fragmentTransaction.commit()日志輸出如下:
可以看到Fragment的生命周期執(zhí)行到了onStart,并且Activity中成功顯示出了當前fragment。
同樣,假設Fragment已經(jīng)執(zhí)行到了onResume方法再為其設置最大生命周期為 STARTED 會怎樣呢?來看日志:
可以看到,設置最大生命周期STARTED后Fragment執(zhí)行了onPause方法,也就是生命周期退回到了onStart。
4. setMaxLifecycle與RESUMED
最后,我們將maxLifecycle設置為RESUMED:
fragment = TestLifecycleFragment.getInstance(Card.DECK[0], 0)val fragmentTransaction = supportFragmentManager.beginTransaction()fragmentTransaction.add(R.id.ll_fragment, fragment)fragmentTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED)fragmentTransaction.commit()可以看到此時和第一種情況一樣的效果,Fragment的生命周期執(zhí)行到了 onResume。
而對于已經(jīng)執(zhí)行了onResume后的Fragment,再去設置最大生命周期為RESUMED會怎么樣呢?因為當前Fragment已經(jīng)是RESUMED狀態(tài)了,所以不會再去執(zhí)行任何代碼。
到這里我們可以得出一個結論:
通過setMaxLifecycle方法可以精確控制Fragment生命周期的狀態(tài),如果Fragment的生命周期狀態(tài)小于被設置的最大生命周期,則當前Fragment的生命周期會執(zhí)行到被設置的最大生命周期;
反之,如果Fragment的生命周期狀態(tài)大于被設置的最大生命周期,那么則會回退到被設置的最大生命周期。
有了這一結論,在ViewPager中便可以對Fragment的生命周期進行控制,以此來更方便的實現(xiàn)延遲加載功能了。
三、Fragment延遲加載的今生
1、延遲加載新方案之于ViewPager
通過上一小節(jié)的分析我們知道了可以通過setMaxLifecycle來設置Fragment的最大生命周期,從而可以實現(xiàn)ViewPager中Fragment的延遲加載。當然,關于生命周期狀態(tài)處理的操作無需我們自己實現(xiàn),在Androidx 1.1.0版本中的FragmentStatePagerAdapter已經(jīng)幫我們實現(xiàn)了,只需要在使用時候傳進去相應的參數(shù)即可。
FragmentStatePagerAdapter的構造方法接收兩個參數(shù),如下:
/** * Constructor for {@link FragmentStatePagerAdapter}. * * If {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT} is passed in, then only the current * Fragment is in the {@link Lifecycle.State#RESUMED} state, while all other fragments are * capped at {@link Lifecycle.State#STARTED}. If {@link #BEHAVIOR_SET_USER_VISIBLE_HINT} is * passed, all fragments are in the {@link Lifecycle.State#RESUMED} state and there will be * callbacks to {@link Fragment#setUserVisibleHint(boolean)}. * * @param fm fragment manager that will interact with this adapter * @param behavior determines if only current fragments are in a resumed state */public FragmentStatePagerAdapter(@NonNull FragmentManager fm, @Behavior int behavior) { mFragmentManager = fm; mBehavior = behavior;}第一個FragmentManager 參數(shù)不必多說,第二個參數(shù)時一個枚舉類型的Behavior參數(shù),其可選值如下:
@Retention(RetentionPolicy.SOURCE)@IntDef({BEHAVIOR_SET_USER_VISIBLE_HINT, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT})private @interface Behavior { }當behavior為BEHAVIOR_SET_USER_VISIBLE_HINT時,Fragment改變的時候,setUserVisibleHint方法會被調(diào)用,也就是這個參數(shù)其實是為了兼容以前的老代碼。并且BEHAVIOR_SET_USER_VISIBLE_HINT參數(shù)已經(jīng)被置為廢棄。所以我們的可選參數(shù)只剩下了BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT。
當behavior為BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT時意味著只有當前顯示的Fragment會被執(zhí)行到onResume,而其它Fragment的生命周期都只會執(zhí)行到onStart。
這一功能時如何實現(xiàn)的呢?我們追隨BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT的腳步找到了setPrimaryItem方法,這個方法的作用是設置ViewPager當前顯示的Item,其源碼如下:
public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) { Fragment fragment = (Fragment)object; 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.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; } }這段代碼非常簡單易懂,mCurrentPrimaryItem是當前正在顯示的item,fragment是接下來要顯示的item。可以看到當mBehavior 為BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT時,mCurrentPrimaryItem的最大生命周期被設置為了STARTED,而fragment的最大生命周期則被設置為了RESUMED。
而當mBehavior為BEHAVIOR_SET_USER_VISIBLE_HINT時仍然會調(diào)用setUserVisibleHint方法,這種情況就不再討論,因為BEHAVIOR_SET_USER_VISIBLE_HINT也已經(jīng)被廢棄掉了。 那么我們著重來分析一下BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT時的情況:
mCurrentPrimaryItem是當前顯示的Fragment,所以該Fragment必然已經(jīng)執(zhí)行到了onResume方法,而此時為其設置了最大生命周期STARTED,那么mCurrentPrimaryItem必然會執(zhí)行onPause退回到STARTED狀態(tài)。 而fragment當前生命周期狀態(tài)為onStart,當為其設置了RESUME的最大生命周期狀態(tài)后,fragment必然會執(zhí)行onResume方法進入RESUMED狀態(tài)。
知道了這一結論后,我們再去進行懶加載的控制是不是就異常簡單了?此時我們只需要一個flag去標志是否是第一次加載數(shù)據(jù)就可以了。
因此,懶加載的實現(xiàn)可以如下:
abstract class TestLifecycleFragment : Fragment() { private var isFirstLoad = true override fun onResume() { super.onResume() if (isFirstLoad) { isFirstLoad = false loadData() } } abstract fun loadData()}2、延遲加載之于ViewPager2
(有機會專門分享一篇講解ViewPager2的文章。)在分析offScreenPageLimit時候得出過這樣一個結論:
ViewPager2的offScreenPageLimit默認值為OFFSCREEN_PAGE_LIMIT_DEFAULT,當setOffscreenPageLimit為OFFSCREEN_PAGE_LIMIT_DEFAULT時候會使用RecyclerView的緩存機制。
默認只會加載當前顯示的Fragment,而不會像ViewPager一樣至少預加載一個item。
當切換到下一個item的時候,當前Fragment會執(zhí)行onPause方法,而下一個Fragment則會從onCreate一直執(zhí)行到onResume。當再次滑動回第一個頁面的時候當前頁面同樣會執(zhí)行onPuase,而第一個頁面會執(zhí)行onResume。
也就是說在ViewPager2中,默認關閉了預加載機制。沒有了預加載機制再談延遲加載其實也沒有任何意義了。所以關于ViewPager2的延遲加載也就不用多說了吧?
只需要將網(wǎng)絡請求放到onStart中即可。相信隨著ViewPager2的普及延遲加載的概念也會慢慢淡出開發(fā)者的視線。
2020/1/4補充:
如果為ViewPager2設置了offScreenPageLimit(1)那結果會是怎樣的呢?我們來看日志:
從日志中可以看到ViewPager2預加載了一個Fragment,并且預加載的Fragment的生命周期僅僅執(zhí)行到了onStart。所以此處我們可以猜測在FragmentStateAdapter中一定設置了setMaxLifecycle(fragment, STARTED),具體源碼不再貼出,大家可以自行查看。因此,此時處理懶加載問題其實和ViewPager的懶加載新方案如出一轍了,僅僅需要添加一個boolean值即可。
三、總結
本篇文章對于Fragment的延遲加載進行了深入的探究,并且了解了在Androidx 1.1.0版本中對Fragment最大生命周期狀態(tài)的控制,從而探究出了Fragment延遲加載的新方案。
對于ViewPager2,因其默認不會進行預加載因此也就意味著我們無需處理ViewPager2的延遲加載問題。好了,這一篇花費了我兩個周末(其實是上周末寫了一半偷了個懶)的文章到此就結束了,如果你從中學有所收獲那么請你不要吝嗇留下你的贊。
相關文章:
Retrofit 用了那么久,動態(tài)代理還不明白
2019年互聯(lián)網(wǎng)公司福利縮水指南
再見2019!你好2020!
今日問題:
Fragment用得溜嘛?
專屬升級社區(qū):《這件事情,我終于想明白了》?
總結
以上是生活随笔為你收集整理的判断当前界面是该fragment_学不动也要学!探究Fragment延迟加载的前世今生的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: kvm虚拟机设置万兆网卡_SR-IOV
- 下一篇: tcpdump抓包ftp协议_tcpdu