Androidx Fragment 懶載入機制實現
在到Androidx之前我們使用support提供的Fragment的懶載入機制,基本上使用的是在setUserVisible + onHiddenChanged 這兩個函式。但是在Androidx下setUserVisible已經被Google官方棄用了,推薦我們使用Fragment.setMaxLifecyCle()的方式來處理Fragment的懶載入。
一、Androidx增加FragmentTransaction.setMaxLifecycle方法控制最大生命週期
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. * @returnthe 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降級到正確的狀態。
二、Fragment 在add+show+hide 模式下的懶載入實現
將需要顯示的 Fragment ,在呼叫 add 或 show 方法後,setMaxLifecycle(showFragment, Lifecycle.State.RESUMED).
將需要隱藏的 Fragment ,在呼叫 hide 方法後,setMaxLifecycle(fragment, Lifecycle.State.STARTED).
切換時的程式碼如下:
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()
其中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巢狀模式下,也能保證正常的懶載入的實現。
三、ViewPager + Fragment 模式下的懶載入實現
在Androidx下,FragmentPagerAdapter、FragmentStatePagerAdapter 類新增了含有behavior的欄位的建構函式,並捨棄了FragmentPagerAdapter(@NonNull FragmentManager fm)方法。
@Deprecated public FragmentPagerAdapter(@NonNull FragmentManager fm) { this(fm, BEHAVIOR_SET_USER_VISIBLE_HINT); } 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 { } @Deprecated public static final int BEHAVIOR_SET_USER_VISIBLE_HINT = 0; 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後,然後Fragment 繼承使用LazyFragment 後,即可實現Androidx下的ViewPager+Fragment的懶載入機制。
這裡我們探究一下設定了 BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT 能實現懶載入的原因。通過看原始碼可以發現,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; } }
四、FragmentPagerAdapter 和 FragmentStatePagerAdapter 區別
1. FragmentPagerAdapter
這兩個Adapter都能配合ViewPager實現Fragment的載入。下面我們來比較一下這兩個類在實現及使用機制上有什麼區別,主要從載入和銷燬兩方面來進行分析。
FragmentPagerAdapter載入Fragment的方法為:instantiateItem,原始碼如下:
@NonNull @Override public Object instantiateItem(@NonNull ViewGroup container, int position) { if (mCurTransaction == null) { mCurTransaction = mFragmentManager.beginTransaction(); } final long itemId = getItemId(position); // Do we already have this fragment? String name = makeFragmentName(container.getId(), itemId); Fragment fragment = mFragmentManager.findFragmentByTag(name); if (fragment != null) { if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment); mCurTransaction.attach(fragment); } else { fragment = getItem(position); if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment); mCurTransaction.add(container.getId(), fragment, makeFragmentName(container.getId(), itemId)); } if (fragment != mCurrentPrimaryItem) { fragment.setMenuVisibility(false); if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED); } else { fragment.setUserVisibleHint(false); } } return fragment; }可以看出來,在instantiateItem方法中,主要是將Fragment新增到FragmentManager中。未新增到FragmentManager中的執行add操作,已新增到FragmentManager中的只進行attach操作。 FragmentPagerAdapter 銷燬 Fragment的方法為:instantiateItem。
@Override public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { Fragment fragment = (Fragment) object; if (mCurTransaction == null) { mCurTransaction = mFragmentManager.beginTransaction(); } mCurTransaction.detach(fragment); if (fragment == mCurrentPrimaryItem) { mCurrentPrimaryItem = null; } }
在destroyItem方法中,只是進行detach操作。detach操作並不會將Fragment銷燬,Fragment依舊是由FragmentManager進行管理。
2.FragmentStatePagerAdapter
FragmentStatePagerAdapter的載入和銷燬的方法名同FragmentPagerAdapter。下面我們先將程式碼放出來:
@NonNull @Override public Object instantiateItem(@NonNull ViewGroup container, int position) { // If we already have this item instantiated, there is nothing // to do. This can happen when we are restoring the entire pager // from its saved state, where the fragment manager has already // taken care of restoring the fragments we previously had instantiated. if (mFragments.size() > position) { Fragment f = mFragments.get(position); if (f != null) { return f; } } if (mCurTransaction == null) { mCurTransaction = mFragmentManager.beginTransaction(); } Fragment fragment = getItem(position); if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment); if (mSavedState.size() > position) { Fragment.SavedState fss = mSavedState.get(position); if (fss != null) { fragment.setInitialSavedState(fss); } } while (mFragments.size() <= position) { mFragments.add(null); } fragment.setMenuVisibility(false); if (mBehavior == BEHAVIOR_SET_USER_VISIBLE_HINT) { fragment.setUserVisibleHint(false); } mFragments.set(position, fragment); mCurTransaction.add(container.getId(), fragment); if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED); } return fragment; }
@Override public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { Fragment fragment = (Fragment) object; if (mCurTransaction == null) { mCurTransaction = mFragmentManager.beginTransaction(); } if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object + " v=" + ((Fragment)object).getView()); while (mSavedState.size() <= position) { mSavedState.add(null); } mSavedState.set(position, fragment.isAdded() ? mFragmentManager.saveFragmentInstanceState(fragment) : null); mFragments.set(position, null); mCurTransaction.remove(fragment); if (fragment == mCurrentPrimaryItem) { mCurrentPrimaryItem = null; } }可以看出來:FragmentStatePagerAdapter是通過一個mFragments陣列來儲存fragment的,通過mSavedState陣列來儲存fragment銷燬時的狀態,通過position獲取到的fragment可能為空(被回收),如果為空,則會再次呼叫getItem方法重新建立新的fragment,然後將mSavedState中儲存的狀態重新賦予這個新的fragment, 達到fragment恢復的效果。當item在頁面中不可見時,該fragment的狀態會先被儲存到mSavedState中,而fragment例項則會被銷燬。
3. 異同比較
相同點:
a). 兩者都會保持當前item(即fragment)和前後的item的狀態。
b).顯示當前item的同時,Adapter會提前初始化後一個item,並把當前item的前一個item儲存在記憶體中。
不同點:
fragment 儲存、恢復、銷燬 的方式不同,FragmentStatePagerAdapter會完全銷燬滑動過去的item,當需要初始化的時候,會重新初始化頁面。FragmentPagerAdapter 則會保留頁面的狀態,並不會完全銷燬掉。
4. 如何選擇
當Viewpager中fragment數量多的時候,為保證效能推薦使用FragmentStatePagerAdapter,反之則推薦使用FragmentPagerAdapter。