詳解Android ViewPager2中的緩存和復(fù)用機(jī)制
1. 前言
眾所周知ViewPager2是ViewPager的替代版本。它解決了ViewPager的一些痛點(diǎn),包括支持right-to-left布局,支持垂直方向滑動(dòng),支持可修改的Fragment集合等。ViewPager2內(nèi)部是使用RecyclerView來(lái)實(shí)現(xiàn)的。
所以它繼承了RecyclerView的優(yōu)勢(shì),包含但不限于以下:
- 支持橫向和垂直方向布局
- 支持嵌套滑動(dòng)
- 支持ItemPrefetch(預(yù)加載)功能
- 支持三級(jí)緩存
ViewPager2相對(duì)于RecyclerView,它又?jǐn)U展出了以下功能
- 支持屏蔽用戶觸摸功能setUserInputEnabled
- 支持模擬拖拽功能fakeDragBy
- 支持離屏顯示功能setOffscreenPageLimit
- 支持顯示Fragment的適配器FragmentStateAdapter
如果熟悉RecyclerView,那么上手ViewPager2將會(huì)非常簡(jiǎn)單。可以簡(jiǎn)單把ViewPager2想象成每個(gè)ItemView都是全屏的RecyclerView。本文將重點(diǎn)講解ViewPager2的離屏顯示功能和基于FragmentStateAdapter的緩存機(jī)制。
2. 回顧RecyclerView緩存機(jī)制

本章節(jié),簡(jiǎn)單回顧下RecyclerView緩存機(jī)制。RecyclerView有三級(jí)緩存,簡(jiǎn)單起見(jiàn),這里只介紹mViewCaches和mRecyclerPool兩種緩存池。更多關(guān)于RecyclerView的緩存原理,請(qǐng)移步公眾號(hào)相關(guān)文章。
- mViewCaches:該緩存離UI更近,效率更高,它的特點(diǎn)是只要position能對(duì)應(yīng)上,就可以直接復(fù)用ViewHolder,無(wú)需重新綁定,該緩存池是用隊(duì)列實(shí)現(xiàn)的,先進(jìn)先出,默認(rèn)大小為2,如果RecyclerView開(kāi)啟了預(yù)抓取功能,則緩存池大小為2+預(yù)抓取個(gè)數(shù),默認(rèn)預(yù)抓取個(gè)數(shù)為1。所以默認(rèn)開(kāi)啟預(yù)抓取緩存池大小為3。
- mRecyclerPool:該緩存池離UI最遠(yuǎn),效率比mViewCaches低,回收到該緩存池的ViewHolder會(huì)將數(shù)據(jù)解綁,當(dāng)復(fù)用該ViewHolder時(shí),需要重新綁定數(shù)據(jù)。它的數(shù)據(jù)結(jié)構(gòu)是類似HashMap。key為itemType,value是數(shù)組,value存儲(chǔ)ViewHolder,數(shù)組默認(rèn)大小為5,最多每種itemType的ViewHolder可以存儲(chǔ)5個(gè)。
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();
}
調(diào)用setOffscreenPageLimit方法就可以為ViewPager2設(shè)置離屏顯示的個(gè)數(shù),默認(rèn)值為-1。如果設(shè)置不當(dāng),會(huì)拋異常。我們看到該方法,只是給mOffscreenPageLimit賦值。為什么就能實(shí)現(xiàn)離屏顯示功能呢?如下代碼
//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;
}
以水平滑動(dòng)ViewPager2為例:getPageSize()表示ViewPager2的寬度,離屏的空間大小為getPageSize() * pageLimit。extraLayoutSpace[0]表示左邊的大小,extraLayoutSpace[1]表示右邊的大小。


假設(shè)設(shè)置offscreenPageLimit為1,簡(jiǎn)單講,Android系統(tǒng)會(huì)默認(rèn)把畫布寬度增加到3倍。左右兩邊各有一個(gè)離屏ViewPager2的寬度。
4. FragmentStateAdapter原理以及緩存機(jī)制
4.1 簡(jiǎn)單使用
FragmentStateAdapter繼承自RecyclerView.Adapter。它有一個(gè)抽象方法,createFragment()。它能將Fragment與ViewPager2完美結(jié)合。
public abstract class FragmentStateAdapter extends
RecyclerView.Adapter<FragmentViewHolder> implements StatefulAdapter {
public abstract Fragment createFragment(int position);
}
使用FragmentStateAdapter非常簡(jiǎn)單,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對(duì)應(yīng)的ViewHolder定義如下,它只是返回一個(gè)簡(jiǎn)單的帶有id的FrameLayout。由此可以看出,F(xiàn)ragmentStateAdapter并不復(fù)用Fragment,它僅僅是復(fù)用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中兩個(gè)非常重要的數(shù)據(jù)結(jié)構(gòu):
final LongSparseArray<Fragment> mFragments = new LongSparseArray<>(); private final LongSparseArray<Integer> mItemIdToViewHolder = new LongSparseArray<>();
mFragments:是position與Fragment的映射表。隨著position的增長(zhǎng),F(xiàn)ragment是會(huì)不斷的新建出來(lái)的。 Fragment可以被緩存起來(lái),當(dāng)它被回收后無(wú)法重復(fù)使用。
Fragment什么時(shí)候會(huì)被回收掉呢?

mItemIdToViewHolder:是position與ViewHolder的Id的映射表。由于ViewHolder是RecyclerView緩存機(jī)制的載體。所以隨著position的增長(zhǎng),ViewHolder并不會(huì)像Fragment那樣不斷的新建出來(lái),而是會(huì)充分利用RecyclerView的復(fù)用機(jī)制。所以如下圖,position 4處打上了一個(gè)大大的問(wèn)號(hào),具體的值是不確定的,它由緩存的大小以及離屏個(gè)數(shù)共同決定的。

接下來(lái)我們講解onViewRecycled()。當(dāng)ViewHolder從mViewCaches緩存中移出到mRecyclerPool緩存中時(shí)會(huì)調(diào)用該方法
@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);
}
}
該方法的作用是,當(dāng)ViewHolder回收到RecyclerPool中時(shí),將ViewHolder相關(guān)的信息從上面兩張表中移除。
舉例 當(dāng)ViewHolder1發(fā)生回收時(shí),position 0對(duì)應(yīng)的信息從兩張表中刪除


最后講解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個(gè)部分:
- 檢查該復(fù)用的ViewHolder在兩張表中是否還有殘留的數(shù)據(jù),如果有,將它從兩張表中移除掉。
- 新建Fragment,并將ViewHolder與Fragment和position的信息注冊(cè)到兩張表中
- 在合適的時(shí)機(jī)把Fragment展示在ViewPager2上。
大概的脈絡(luò)就是這樣,為了避免文章冗余,其它的細(xì)支且也蠻重要的方法就沒(méi)有列出來(lái)
5. 案例講解回收機(jī)制
5.1 默認(rèn)情況
默認(rèn)情況:offscreenPageLimit = -1,開(kāi)啟預(yù)抓取功能
因?yàn)殚_(kāi)啟了預(yù)抓取,所以mViewCaches大小為3。

- 剛開(kāi)始進(jìn)入ViewPager2,沒(méi)有觸發(fā)Touch事件,不會(huì)觸發(fā)預(yù)抓取,所以只有Fragment1
- 滑動(dòng)到Fragment2,會(huì)觸發(fā)Fragment3預(yù)抓取,由于offscreenPageLimit = -1,所以只有Fragment2會(huì)展示在ViewPager2上,1和3進(jìn)入mViewCaches緩存中
- 滑動(dòng)到Fragment3。1、2、4進(jìn)入mViewCaches緩存中
- 滑動(dòng)到Fragment4。2、3、5進(jìn)入mViewCaches緩存中,由于緩存數(shù)量為3,所以1被擠出到mRecyclerPool緩存中,同時(shí)把Fragment1從mFragments中移除掉
- 滑動(dòng)到Fragment5。Fragment6會(huì)復(fù)用Fragment1對(duì)應(yīng)的ViewHolder。3、4、6進(jìn)入mViewCaches緩存中,2被擠出到mRecyclerPool緩存中
5.2 offscreenPageLimit=1

offscreenPageLimit=1,所以ViewPager2一下子能展示3屏Fragment,左右各顯示一屏
- Fragment1左邊沒(méi)有數(shù)據(jù),所以屏幕只有1和2
- 滑動(dòng)到fragment2,1、2、3顯示在屏幕上(1和3肉眼不可見(jiàn),下同),同時(shí)預(yù)抓取4放入mViewCaches
- 滑動(dòng)到fragment3,2、3、4顯示在屏幕上,1和5放入mViewCaches
- 滑動(dòng)到fragment4,3、4、5顯示在屏幕上,1、2、6放入mViewCaches
- 滑動(dòng)到fragment5,4、5、6顯示在屏幕上,2、3、7放入mViewCaches,1被回收到mRecyclerPool緩存中。Fragment1同時(shí)從mFragments中刪除掉
總結(jié)
到此這篇關(guān)于Android ViewPager2中緩存和復(fù)用機(jī)制的文章就介紹到這了,更多相關(guān)ViewPager2緩存和復(fù)用機(jī)制內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android Studio修改Log信息顏色的實(shí)現(xiàn)
這篇文章主要介紹了Android Studio修改Log信息顏色的實(shí)現(xiàn),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-04-04
詳解Android中Handler的實(shí)現(xiàn)原理
這篇文章主要為大家詳細(xì)介紹了Android中Handler的實(shí)現(xiàn)原理,本文深入分析 Android 的消息處理機(jī)制,了解 Handler 的工作原理,感興趣的小伙伴們可以參考一下2016-04-04
Android MonoRepo多倉(cāng)和單倉(cāng)的差別理論
這篇文章主要為大家介紹了Android MonoRepo多倉(cāng)和單倉(cāng)的差別理論,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06
Android為按鈕控件綁定事件的五種實(shí)現(xiàn)方式
本篇文章主要是介紹了Android為按鈕控件綁定事件的五種實(shí)現(xiàn)方式,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2016-11-11
Android實(shí)現(xiàn)漸變色的圓弧虛線效果
最近在學(xué)習(xí)Android的paint類的時(shí)候,學(xué)習(xí)了PathEffect路徑效果和Shader渲染效果。所以就做了下面的一個(gè)效果,其中自定義的view組主要是用DashPathEffect、SweepGradient的API形成的效果。感興趣的朋友們可以參考借鑒,下面來(lái)一起看看吧。2016-10-10

