詳解Android ViewPager2中的緩存和復用機制
1. 前言
眾所周知ViewPager2是ViewPager的替代版本。它解決瞭ViewPager的一些痛點,包括支持right-to-left佈局,支持垂直方向滑動,支持可修改的Fragment集合等。ViewPager2內部是使用RecyclerView來實現的。
所以它繼承瞭RecyclerView的優勢,包含但不限於以下:
- 支持橫向和垂直方向佈局
- 支持嵌套滑動
- 支持ItemPrefetch(預加載)功能
- 支持三級緩存
ViewPager2相對於RecyclerView,它又擴展出瞭以下功能
- 支持屏蔽用戶觸摸功能setUserInputEnabled
- 支持模擬拖拽功能fakeDragBy
- 支持離屏顯示功能setOffscreenPageLimit
- 支持顯示Fragment的適配器FragmentStateAdapter
如果熟悉RecyclerView,那麼上手ViewPager2將會非常簡單。可以簡單把ViewPager2想象成每個ItemView都是全屏的RecyclerView。本文將重點講解ViewPager2的離屏顯示功能和基於FragmentStateAdapter的緩存機制。
2. 回顧RecyclerView緩存機制
本章節,簡單回顧下RecyclerView緩存機制。RecyclerView有三級緩存,簡單起見,這裡隻介紹mViewCaches和mRecyclerPool兩種緩存池。更多關於RecyclerView的緩存原理,請移步公眾號相關文章。
- mViewCaches:該緩存離UI更近,效率更高,它的特點是隻要position能對應上,就可以直接復用ViewHolder,無需重新綁定,該緩存池是用隊列實現的,先進先出,默認大小為2,如果RecyclerView開啟瞭預抓取功能,則緩存池大小為2+預抓取個數,默認預抓取個數為1。所以默認開啟預抓取緩存池大小為3。
- mRecyclerPool:該緩存池離UI最遠,效率比mViewCaches低,回收到該緩存池的ViewHolder會將數據解綁,當復用該ViewHolder時,需要重新綁定數據。它的數據結構是類似HashMap。key為itemType,value是數組,value存儲ViewHolder,數組默認大小為5,最多每種itemType的ViewHolder可以存儲5個。
3. offscreenPageLimit原理
//androidx.viewpager2:ViewPager2:1.0.0@aar //ViewPager2.java public void setOffscreenPageLimit(@OffscreenPageLimit int limit) { if (limit < 1 && limit != OFFSCREEN_PAGE_LIMIT_DEFAULT) { throw new IllegalArgumentException( "Offscreen page limit must be OFFSCREEN_PAGE_LIMIT_DEFAULT or a number > 0"); } mOffscreenPageLimit = limit; mRecyclerView.requestLayout(); }
調用setOffscreenPageLimit方法就可以為ViewPager2設置離屏顯示的個數,默認值為-1。如果設置不當,會拋異常。我們看到該方法,隻是給mOffscreenPageLimit賦值。為什麼就能實現離屏顯示功能呢?如下代碼
//androidx.viewpager2:ViewPager2:1.0.0@aar //ViewPager2$LinearLayoutManagerImpl @Override protected void calculateExtraLayoutSpace(@NonNull RecyclerView.State state, @NonNull int[] extraLayoutSpace) { int pageLimit = getOffscreenPageLimit(); if (pageLimit == OFFSCREEN_PAGE_LIMIT_DEFAULT) { super.calculateExtraLayoutSpace(state, extraLayoutSpace); return; } final int offscreenSpace = getPageSize() * pageLimit; extraLayoutSpace[0] = offscreenSpace; extraLayoutSpace[1] = offscreenSpace; }
以水平滑動ViewPager2為例:getPageSize()表示ViewPager2的寬度,離屏的空間大小為getPageSize() * pageLimit。extraLayoutSpace[0]表示左邊的大小,extraLayoutSpace[1]表示右邊的大小。
假設設置offscreenPageLimit為1,簡單講,Android系統會默認把畫佈寬度增加到3倍。左右兩邊各有一個離屏ViewPager2的寬度。
4. FragmentStateAdapter原理以及緩存機制
4.1 簡單使用
FragmentStateAdapter繼承自RecyclerView.Adapter。它有一個抽象方法,createFragment()。它能將Fragment與ViewPager2完美結合。
public abstract class FragmentStateAdapter extends RecyclerView.Adapter<FragmentViewHolder> implements StatefulAdapter { public abstract Fragment createFragment(int position); }
使用FragmentStateAdapter非常簡單,Demo如下
class ViewPager2WithFragmentsActivity : AppCompatActivity() { private lateinit var mViewPager2: ViewPager2 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_recycler_view_view_pager2) mViewPager2 = findViewById(R.id.viewPager2) (mViewPager2.getChildAt(0) as RecyclerView).layoutManager?.apply { // isItemPrefetchEnabled = false } mViewPager2.orientation = ViewPager2.ORIENTATION_VERTICAL mViewPager2.adapter = MyAdapter(this) // mViewPager2.offscreenPageLimit = 1 } inner class MyAdapter(fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity) { override fun getItemCount(): Int { return 100 } override fun createFragment(position: Int): Fragment { return MyFragment("Item $position") } } class MyFragment(val text: String) : Fragment() { init { println("MyFragment $text") } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { var view = layoutInflater.inflate(R.layout.view_item_view_pager_snap, container) view.findViewById<TextView>(R.id.text_view).text = text return view; } } }
4.2 原理
首先FragmentStateAdapter對應的ViewHolder定義如下,它隻是返回一個簡單的帶有id的FrameLayout。由此可以看出,FragmentStateAdapter並不復用Fragment,它僅僅是復用FrameLayout而已。
public final class FragmentViewHolder extends ViewHolder { private FragmentViewHolder(@NonNull FrameLayout container) { super(container); } @NonNull static FragmentViewHolder create(@NonNull ViewGroup parent) { FrameLayout container = new FrameLayout(parent.getContext()); container.setLayoutParams( new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); container.setId(ViewCompat.generateViewId()); container.setSaveEnabled(false); return new FragmentViewHolder(container); } @NonNull FrameLayout getContainer() { return (FrameLayout) itemView; } }
然後介紹FragmentStateAdapter中兩個非常重要的數據結構:
final LongSparseArray<Fragment> mFragments = new LongSparseArray<>(); private final LongSparseArray<Integer> mItemIdToViewHolder = new LongSparseArray<>();
mFragments:是position與Fragment的映射表。隨著position的增長,Fragment是會不斷的新建出來的。 Fragment可以被緩存起來,當它被回收後無法重復使用。
Fragment什麼時候會被回收掉呢?
mItemIdToViewHolder:是position與ViewHolder的Id的映射表。由於ViewHolder是RecyclerView緩存機制的載體。所以隨著position的增長,ViewHolder並不會像Fragment那樣不斷的新建出來,而是會充分利用RecyclerView的復用機制。所以如下圖,position 4處打上瞭一個大大的問號,具體的值是不確定的,它由緩存的大小以及離屏個數共同決定的。
接下來我們講解onViewRecycled()。當ViewHolder從mViewCaches緩存中移出到mRecyclerPool緩存中時會調用該方法
@Override public final void onViewRecycled(@NonNull FragmentViewHolder holder) { final int viewHolderId = holder.getContainer().getId(); final Long boundItemId = itemForViewHolder(viewHolderId); // item currently bound to the VH if (boundItemId != null) { removeFragment(boundItemId); mItemIdToViewHolder.remove(boundItemId); } }
該方法的作用是,當ViewHolder回收到RecyclerPool中時,將ViewHolder相關的信息從上面兩張表中移除。
舉例 當ViewHolder1發生回收時,position 0對應的信息從兩張表中刪除
最後講解onBindViewHolder方法
@Override public final void onBindViewHolder(final @NonNull FragmentViewHolder holder, int position) { final long itemId = holder.getItemId(); final int viewHolderId = holder.getContainer().getId(); final Long boundItemId = itemForViewHolder(viewHolderId); // item currently bound to the VH if (boundItemId != null && boundItemId != itemId) { removeFragment(boundItemId); mItemIdToViewHolder.remove(boundItemId); } mItemIdToViewHolder.put(itemId, viewHolderId); // this might overwrite an existing entry ensureFragment(position); /** Special case when {@link RecyclerView} decides to keep the {@link container} * attached to the window, but not to the view hierarchy (i.e. parent is null) */ final FrameLayout container = holder.getContainer(); if (ViewCompat.isAttachedToWindow(container)) { if (container.getParent() != null) { throw new IllegalStateException("Design assumption violated."); } container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { if (container.getParent() != null) { container.removeOnLayoutChangeListener(this); placeFragmentInViewHolder(holder); } } }); } gcFragments(); }
該方法可以分成3個部分:
- 檢查該復用的ViewHolder在兩張表中是否還有殘留的數據,如果有,將它從兩張表中移除掉。
- 新建Fragment,並將ViewHolder與Fragment和position的信息註冊到兩張表中
- 在合適的時機把Fragment展示在ViewPager2上。
大概的脈絡就是這樣,為瞭避免文章冗餘,其它的細支且也蠻重要的方法就沒有列出來
5. 案例講解回收機制
5.1 默認情況
默認情況:offscreenPageLimit = -1,開啟預抓取功能
因為開啟瞭預抓取,所以mViewCaches大小為3。
- 剛開始進入ViewPager2,沒有觸發Touch事件,不會觸發預抓取,所以隻有Fragment1
- 滑動到Fragment2,會觸發Fragment3預抓取,由於offscreenPageLimit = -1,所以隻有Fragment2會展示在ViewPager2上,1和3進入mViewCaches緩存中
- 滑動到Fragment3。1、2、4進入mViewCaches緩存中
- 滑動到Fragment4。2、3、5進入mViewCaches緩存中,由於緩存數量為3,所以1被擠出到mRecyclerPool緩存中,同時把Fragment1從mFragments中移除掉
- 滑動到Fragment5。Fragment6會復用Fragment1對應的ViewHolder。3、4、6進入mViewCaches緩存中,2被擠出到mRecyclerPool緩存中
5.2 offscreenPageLimit=1
offscreenPageLimit=1,所以ViewPager2一下子能展示3屏Fragment,左右各顯示一屏
- Fragment1左邊沒有數據,所以屏幕隻有1和2
- 滑動到fragment2,1、2、3顯示在屏幕上(1和3肉眼不可見,下同),同時預抓取4放入mViewCaches
- 滑動到fragment3,2、3、4顯示在屏幕上,1和5放入mViewCaches
- 滑動到fragment4,3、4、5顯示在屏幕上,1、2、6放入mViewCaches
- 滑動到fragment5,4、5、6顯示在屏幕上,2、3、7放入mViewCaches,1被回收到mRecyclerPool緩存中。Fragment1同時從mFragments中刪除掉
總結
到此這篇關於Android ViewPager2中緩存和復用機制的文章就介紹到這瞭,更多相關ViewPager2緩存和復用機制內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- Android如何使用ViewPager2實現頁面滑動切換效果
- Android性能優化之ViewPagers + Fragment緩存優化
- 深入瞭解ViewPager2的使用
- Android用viewPager2實現UI界面翻頁滾動的效果
- 安卓開發之FragmentPagerAdapter和FragmentStatePagerAdapter詳解