前言
RecyclerView這個控件幾乎所有的Android開發(fā)者都使用過(甚至不用加幾乎),它是真的很好用,完美取代了ListView和GridView,而RecyclerView之所以好用,得益于它優(yōu)秀的緩存機(jī)制。關(guān)于RecyclerView緩存機(jī)制,更是需要我們開發(fā)者來掌握的。本文就將先從整體流程看RecyclerView的緩存,再帶你從源碼角度分析,跳過讀源碼的坑,最后用一個簡單的demo的形式展示出來。在開始RecyclerView的緩存機(jī)制之前我們先學(xué)習(xí)關(guān)于ViewHolder的知識。
RecyclerView為什么強(qiáng)制我們實(shí)現(xiàn)ViewHolder模式?
關(guān)于這個問題,我們首先看一下ListView。ListView是不強(qiáng)制我們實(shí)現(xiàn)ViewHolder的,但是后來google建議我們實(shí)現(xiàn)ViewHolder模式。我們先分別看一下這兩種不同的方式。


其實(shí)這里我已經(jīng)用紅框標(biāo)出來了,ListView使用ViewHolder的好處就在于可以避免每次getView都進(jìn)行findViewById()操作,因?yàn)閒indViewById()利用的是DFS算法(深度優(yōu)化搜索),是非常耗性能的。而對于RecyclerView來說,強(qiáng)制實(shí)現(xiàn)ViewHolder的其中一個原因就是避免多次進(jìn)行findViewById()的處理,另一個原因就是因?yàn)镮temView和ViewHolder的關(guān)系是一對一,也就是說一個ViewHolder對應(yīng)一個ItemView。這個ViewHolder當(dāng)中持有對應(yīng)的ItemView的所有信息,比如說:position;view;width等等,拿到了ViewHolder基本就拿到了ItemView的所有信息,而ViewHolder使用起來相比itemView更加方便。RecyclerView緩存機(jī)制緩存的就是ViewHolder(ListView緩存的是ItemView),這也是為什么RecyclerView為什么強(qiáng)制我們實(shí)現(xiàn)ViewHolder的原因。
Listview的緩存機(jī)制
在正式講RecyclerView的緩存機(jī)制之前還需要提一嘴ListView的緩存機(jī)制,不多BB,先上圖

ListView的緩存有兩級,在ListView里面有一個內(nèi)部類 RecycleBin,RecycleBin有兩個對象Active View和Scrap View來管理緩存,Active View是第一級,Scrap View是第二級。
- Active View:是緩存在屏幕內(nèi)的ItemView,當(dāng)列表數(shù)據(jù)發(fā)生變化時,屏幕內(nèi)的數(shù)據(jù)可以直接拿來復(fù)用,無須進(jìn)行數(shù)據(jù)綁定。
-
Scrap view:緩存屏幕外的ItemView,這里所有的緩存的數(shù)據(jù)都是"臟的",也就是數(shù)據(jù)需要重新綁定,也就是說屏幕外的所有數(shù)據(jù)在進(jìn)入屏幕的時候都要走一遍getView()方法。
再來一張圖,看看ListView的緩存流程
lisview.png
當(dāng)Active View和Scrap View中都沒有緩存的時候就會直接create view。
小結(jié)
ListView的緩存機(jī)制相對比較好理解,它只有兩級緩存,一級緩存Active View是負(fù)責(zé)屏幕內(nèi)的ItemView快速復(fù)用,而Scrap View是緩存屏幕外的數(shù)據(jù),當(dāng)該數(shù)據(jù)從屏幕外滑動到屏幕內(nèi)的時候需要走一遍getView()方法。
RecyclerView的緩存機(jī)制
先上圖

RecyclerView的緩存分為四級
- Scrap
- Cache
- ViewCacheExtension
- RecycledViewPool
Scrap對應(yīng)ListView 的Active View,就是屏幕內(nèi)的緩存數(shù)據(jù),就是相當(dāng)于換了個名字,可以直接拿來復(fù)用。
Cache 剛剛移出屏幕的緩存數(shù)據(jù),默認(rèn)大小是2個,當(dāng)其容量被充滿同時又有新的數(shù)據(jù)添加的時候,會根據(jù)FIFO原則,把優(yōu)先進(jìn)入的緩存數(shù)據(jù)移出并放到下一級緩存中,然后再把新的數(shù)據(jù)添加進(jìn)來。Cache里面的數(shù)據(jù)是干凈的,也就是攜帶了原來的ViewHolder的所有數(shù)據(jù)信息,數(shù)據(jù)可以直接來拿來復(fù)用。需要注意的是,cache是根據(jù)position來尋找數(shù)據(jù)的,這個postion是根據(jù)第一個或者最后一個可見的item的position以及用戶操作行為(上拉還是下拉)。
舉個栗子:當(dāng)前屏幕內(nèi)第一個可見的item的position是1,用戶進(jìn)行了一個下拉操作,那么當(dāng)前預(yù)測的position就相當(dāng)于(1-1=0),也就是position=0的那個item要被拉回到屏幕,此時RecyclerView就從Cache里面找position=0的數(shù)據(jù),如果找到了就直接拿來復(fù)用。
ViewCacheExtension是google留給開發(fā)者自己來自定義緩存的,這個ViewCacheExtension我個人建議還是要慎用,因?yàn)槲野抢抢W(wǎng)上其他的博客,沒有找到對應(yīng)的使用場景,而且這個類的api設(shè)計(jì)的也有些奇怪,只有一個public abstract View getViewForPositionAndType(@NonNull Recycler recycler, int position, int type);讓開發(fā)者重寫通過position和type拿到ViewHolder的方法,卻沒有提供如何產(chǎn)生ViewHolder或者管理ViewHolder的方法,給人一種只出不進(jìn)的趕腳,還是那句話慎用。
RecycledViewPool剛才說了Cache默認(rèn)的緩存數(shù)量是2個,當(dāng)Cache緩存滿了以后會根據(jù)FIFO(先進(jìn)先出)的規(guī)則把Cache先緩存進(jìn)去的ViewHolder移出并緩存到RecycledViewPool中,RecycledViewPool默認(rèn)的緩存數(shù)量是5個。RecycledViewPool與Cache相比不同的是,從Cache里面移出的ViewHolder再存入RecycledViewPool之前ViewHolder的數(shù)據(jù)會被全部重置,相當(dāng)于一個新的ViewHolder,而且Cache是根據(jù)position來獲取ViewHolder,而RecycledViewPool是根據(jù)itemType獲取的,如果沒有重寫getItemType()方法,itemType就是默認(rèn)的。因?yàn)镽ecycledViewPool緩存的ViewHolder是全新的,所以取出來的時候需要走onBindViewHolder()方法。
再來張圖看看整體流程

這里大家先記住主要流程,并且記住各級緩存是根據(jù)什么拿到ViewHolder以及ViewHolder能否直接拿來復(fù)用,先有一個整體的認(rèn)識,下面我會帶著大家再簡單分析一下RecyclerView緩存機(jī)制的源碼。
閱讀RecyclerView的緩存機(jī)制源碼
由于篇幅和內(nèi)容的關(guān)系,我不可能帶大家一行一行讀,這里我只列出關(guān)鍵點(diǎn),還有哪些需要重點(diǎn)看,哪些可以直接略過,避免大家陷入讀源碼一個勁兒鉆進(jìn)去出不來的誤區(qū)。
當(dāng)RecyclerView繪制的時候,會走到LayoutManager里面的next()方法,在next()里面是正式開始使用緩存機(jī)制,這里以LinearLayoutManager為例子
/**
* Gets the view for the next element that we should layout.
* Also updates current item index to the next item, based on {@link #mItemDirection}
*
* @return The next element that we should layout.
*/
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
在next方法里傳入了Recycler對象,這個對象是RecyclerView的內(nèi)部類。我們先去看一眼這個類
public final class Recycler {
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
ArrayList<ViewHolder> mChangedScrap = null;
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
private final List<ViewHolder>
mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);
private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
int mViewCacheMax = DEFAULT_CACHE_SIZE;
RecycledViewPool mRecyclerPool;
private ViewCacheExtension mViewCacheExtension;
static final int DEFAULT_CACHE_SIZE = 2;
}
再看一眼RecycledViewPool的源碼
public static class RecycledViewPool {
private static final int DEFAULT_MAX_SCRAP = 5;
static class ScrapData {
final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
long mCreateRunningAverageNs = 0;
long mBindRunningAverageNs = 0;
}
SparseArray<ScrapData> mScrap = new SparseArray<>();
其中mAttachedScrap對應(yīng)Scrap;mCachedViews對應(yīng)Cache;mViewCacheExtension對應(yīng)ViewCacheExtension;mRecyclerPool對應(yīng)RecycledViewPool。
注意:mAttachedScrap、mCachedViews和RecycledViewPool里面的mScrapHeap都是ArrayList,緩存被加入到這三個對象里面實(shí)際上就是調(diào)用的ArrayList.add()方法,復(fù)用緩存呢,這里要注意一下不是調(diào)用的ArrayList.get()而是ArrayList.remove(),其實(shí)這里也很好理解,因?yàn)楫?dāng)緩存數(shù)據(jù)被取出來展示到了屏幕內(nèi),自然就應(yīng)該被移除。
我們現(xiàn)在回到剛才的next()方法里,recycler.getViewForPosition(mCurrentPosition); 直接去看getViewForPosition這個方法,接著跟到了這里
View getViewForPosition(int position, boolean dryRun) {
return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
接著跟進(jìn)去
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
if (position < 0 || position >= mState.getItemCount()) {
throw new IndexOutOfBoundsException("Invalid item position " + position
+ "(" + position + "). Item count:" + mState.getItemCount()
+ exceptionLabel());
}
boolean fromScrapOrHiddenOrCache = false;
ViewHolder holder = null;
// 0) If there is a changed scrap, try to find from there
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
// 1) Find by position from scrap/hidden list/cache
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
}
if (holder == null) {
final int type = mAdapter.getItemViewType(offsetPosition);
// 2) Find from scrap/cache via stable ids, if exists
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
if (holder != null) {
// update position
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
}
}
if (holder == null && mViewCacheExtension != null) {
// We are NOT sending the offsetPosition because LayoutManager does not
// know it.
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
}
}
if (holder == null) { // fallback to pool
if (DEBUG) {
Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
+ position + ") fetching from shared pool");
}
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
if (holder == null) {
long start = getNanoTime();
if (deadlineNs != FOREVER_NS
&& !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
// abort - we have a deadline we can't meet
return null;
}
holder = mAdapter.createViewHolder(RecyclerView.this, type);
if (ALLOW_THREAD_GAP_WORK) {
// only bother finding nested RV if prefetching
RecyclerView innerView = findNestedRecyclerView(holder.itemView);
if (innerView != null) {
holder.mNestedRecyclerView = new WeakReference<>(innerView);
}
}
}
}
boolean bound = false;
if (mState.isPreLayout() && holder.isBound()) {
// do not update unless we absolutely have to.
holder.mPreLayoutPosition = position;
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
if (DEBUG && holder.isRemoved()) {
throw new IllegalStateException("Removed holder should be bound and it should"
+ " come here only in pre-layout. Holder: " + holder
+ exceptionLabel());
}
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
return holder;
}
終于到了緩存機(jī)制最核心的地方,為了方便大家閱讀,我對這部分源碼進(jìn)行了刪減,直接從官方給的注釋里面看。
// (0) If there is a changed scrap, try to find from there
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
這里面只有設(shè)置動畫以后才會為true,跟咱們講的緩存也沒有多大關(guān)系,直接略過。
// 1) Find by position from scrap/hidden list/cache
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
}
這里就開始拿第一級和第二級緩存了getScrapOrHiddenOrCachedHolderForPosition()這個方法可以深入去看以下,注意這里傳的參數(shù)是position(dryRun這個參數(shù)不用管),就跟我之前說的,Scrap和Cache是根據(jù)position拿到緩存。
if (holder == null && mViewCacheExtension != null) {
// We are NOT sending the offsetPosition because LayoutManager does not
// know it.
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
}
}
這里開始拿第三級緩存了,這里我們不自定義ViewCacheExtension就不會進(jìn)入判斷條件,還是那句話慎用。
if (holder == null) { // fallback to pool
if (DEBUG) {
Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
+ position + ") fetching from shared pool");
}
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
這里到了第四級緩存RecycledViewPool,getRecycledViewPool().getRecycledView(type);通過type拿到ViewHolder,接著holder.resetInternal();重置ViewHolder,讓其變成一個全新的ViewHolder
if (holder == null) {
long start = getNanoTime();
if (deadlineNs != FOREVER_NS
&& !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
// abort - we have a deadline we can't meet
return null;
}
holder = mAdapter.createViewHolder(RecyclerView.this, type);
if (ALLOW_THREAD_GAP_WORK) {
// only bother finding nested RV if prefetching
RecyclerView innerView = findNestedRecyclerView(holder.itemView);
if (innerView != null) {
holder.mNestedRecyclerView = new WeakReference<>(innerView);
}
}
}
到這里如果ViewHolder還為null的話,就會create view了,創(chuàng)建一個新的ViewHolder
boolean bound = false;
if (mState.isPreLayout() && holder.isBound()) {
// do not update unless we absolutely have to.
holder.mPreLayoutPosition = position;
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
if (DEBUG && holder.isRemoved()) {
throw new IllegalStateException("Removed holder should be bound and it should"
+ " come here only in pre-layout. Holder: " + holder
+ exceptionLabel());
}
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
這里else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid())是判斷這個ViewHolder是不是有效的,也就是可不可以復(fù)用,如果不可以復(fù)用就會進(jìn)入tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);這個方法,在這里面調(diào)用了bindViewHolder()方法。
點(diǎn)進(jìn)去看一眼
private boolean tryBindViewHolderByDeadline(@NonNull ViewHolder holder, int offsetPosition,
int position, long deadlineNs) {
....................
mAdapter.bindViewHolder(holder, offsetPosition);
....................
return true;
}
在點(diǎn)進(jìn)去就到了我們熟悉的onBindViewHolder()
public final void bindViewHolder(@NonNull VH holder, int position) {
.......................
onBindViewHolder(holder, position, holder.getUnmodifiedPayloads());
.........................
}
至此,緩存機(jī)制的整體流程就全部分析完畢了。
小結(jié)
ListView有兩級緩存,分別是Active View和Scrap View,緩存的對象是ItemView;而RecyclerView有四級緩存,分別是Scrap、Cache、ViewCacheExtension和RecycledViewPool,緩存的對象是ViewHolder。Scrap和Cache分別是通過position去找ViewHolder可以直接復(fù)用;ViewCacheExtension自定義緩存,目前來說應(yīng)用場景比較少卻需慎用;RecycledViewPool通過type來獲取ViewHolder,獲取的ViewHolder是個全新,需要重新綁定數(shù)據(jù)。當(dāng)你看到這里的時候,面試官再問RecyclerView的性能比ListView優(yōu)化在哪里,我想你已經(jīng)有答案。
通過Demo再理解一遍
擔(dān)心你看完上面的內(nèi)容,倒頭就忘,我們寫個簡單的demo通過打印log的方式來鞏固一下學(xué)到的知識。
簡單說一下Demo里面需要注意的代碼,下面是對RecyclerView的一個包裝
public class RecyclerViewWrapper extends RecyclerView {
private LayoutListener layoutListener;
public RecyclerViewWrapper(@NonNull Context context) {
super(context);
}
public RecyclerViewWrapper(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public RecyclerViewWrapper(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public void setLayoutListener(LayoutListener layoutListener) {
this.layoutListener = layoutListener;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (layoutListener != null) {
layoutListener.onBeforeLayout();
}
super.onLayout(changed, l, t, r, b);
if (layoutListener != null) {
layoutListener.onAfterLayout();
}
}
public interface LayoutListener {
void onBeforeLayout();
void onAfterLayout();
}
}
其實(shí)很簡單,在RecyclerView執(zhí)行onLayout()方法前后執(zhí)行一下咱們打印緩存變化的方法
再看一眼打印緩存變化的方法,利用反射的技術(shù)
/**
* 利用java反射機(jī)制拿到RecyclerView內(nèi)的緩存并打印出來
* */
private void showMessage(RecyclerViewWrapper rv) {
try {
Field mRecycler =
Class.forName("androidx.recyclerview.widget.RecyclerView").getDeclaredField("mRecycler");
mRecycler.setAccessible(true);
RecyclerView.Recycler recyclerInstance = (RecyclerView.Recycler) mRecycler.get(rv);
Class<?> recyclerClass = Class.forName(mRecycler.getType().getName());
Field mViewCacheMax = recyclerClass.getDeclaredField("mViewCacheMax");
Field mAttachedScrap = recyclerClass.getDeclaredField("mAttachedScrap");
Field mChangedScrap = recyclerClass.getDeclaredField("mChangedScrap");
Field mCachedViews = recyclerClass.getDeclaredField("mCachedViews");
Field mRecyclerPool = recyclerClass.getDeclaredField("mRecyclerPool");
mViewCacheMax.setAccessible(true);
mAttachedScrap.setAccessible(true);
mChangedScrap.setAccessible(true);
mCachedViews.setAccessible(true);
mRecyclerPool.setAccessible(true);
int mViewCacheSize = (int) mViewCacheMax.get(recyclerInstance);
ArrayListWrapper<RecyclerView.ViewHolder> mAttached =
(ArrayListWrapper<RecyclerView.ViewHolder>) mAttachedScrap.get(recyclerInstance);
ArrayList<RecyclerView.ViewHolder> mChanged =
(ArrayList<RecyclerView.ViewHolder>) mChangedScrap.get(recyclerInstance);
ArrayList<RecyclerView.ViewHolder> mCached =
(ArrayList<RecyclerView.ViewHolder>) mCachedViews.get(recyclerInstance);
RecyclerView.RecycledViewPool recycledViewPool =
(RecyclerView.RecycledViewPool) mRecyclerPool.get(recyclerInstance);
Class<?> recyclerPoolClass = Class.forName(mRecyclerPool.getType().getName());
Log.e(TAG, "mAttachedScrap(一緩) size is:" + mAttached.maxSize + ", \n" + "mCachedViews(二緩) max size is:" + mViewCacheSize + ","
+ getMCachedViewsInfo(mCached) + getRVPoolInfo(recyclerPoolClass, recycledViewPool));
} catch (Exception e) {
e.printStackTrace();
}
}
核心的代碼呢就這兩塊,文章的最后我會把我的demo上傳到github上。
注意:本文使用的RecyclerView的版本是androidx,在調(diào)onAttachedToWindow()方法的時候會進(jìn)行版本判斷,如果是5.0以及以上的系統(tǒng)(即大于等于21),GapWorker會把RecyclerView自己加入到GapWorker。在RenderThread線程執(zhí)行預(yù)取操作的時候會mPrefetchMaxCountObserved = 1,這就會導(dǎo)致你使用5.0以及以上系統(tǒng)的手機(jī)打印緩存數(shù)量的時候會比你預(yù)想的多一個。這里為了不造成這種問題,本文使用4.4系統(tǒng)的Android模擬器來演示Demo。
Demo演示效果截圖
-
啟動App,第一次加載的情況
第一次啟動app加載
初始化加載只有屏幕內(nèi)的一級緩存7個
-
把position = 0 和position=1 兩個item移除屏幕
前兩個item移除屏幕
看藍(lán)色框出來的,position = 0 和position = 1的item被加入到了Cache緩存中,Cache的緩存數(shù)量我沒有修改,默認(rèn)2個,也就說現(xiàn)在已經(jīng)滿了
-
再把position = 2的item也移除屏幕
position = 2的item也移除屏幕
因?yàn)樯弦徊紺ache里面的緩存已經(jīng)慢了,此時position = 2又被加入緩存,根據(jù)FIFO的原則,cache里面position = 0 被remove掉并加入到了四級緩存RecycledView里面,此時RecycledView也有了緩存并且該緩存沒有任何有效數(shù)據(jù)信息。
-
再上一步的基礎(chǔ)上下拉一下,把position = 2的item顯示出來
把position = 2的item顯示出來
此時position = 2的item將要被顯示出來,會先從cache里面找,發(fā)現(xiàn)Cache正好有position = 2的緩存就直接拿出來復(fù)用了,并且原來在屏幕里的position= 9 的item被移除了,就會加入到Cache的緩存里
-----------------------------------分割線-------------------------------------------
現(xiàn)在看一下onCreateViewHolder()和onBindViewHolder()的情況 -
還是啟動App,第一次加載后,再把position = 0和position =1的item移除屏幕再移回來
顯示效果onBindViewHolder()方法沒有被重復(fù)執(zhí)行(靜態(tài)圖顯示的效果不是很好,gif錄制的質(zhì)量太差了,還是建議下載demo自己嘗試一下)
-
最后留一個問題給大家
onCreateViewHolder不再執(zhí)行
為什么在第10次onCreateViewHolder()執(zhí)行以后就再也沒有執(zhí)行過onCreateViewHolder()方法了?
總結(jié)
關(guān)于RecyclerView的緩存分為四級,Scrap、Cache、ViewCacheExtension和RecycledViewPool。Scrap是屏幕內(nèi)的緩存一般我們不怎么需要特別注意;Cache可直接拿來復(fù)用的緩存,性能高效;ViewCacheExtension需要開發(fā)者自定義的緩存,API設(shè)計(jì)比較奇怪,慎用;RecycledViewPool四級緩存,可以避免用戶調(diào)用onCreateViewHolder()方法,提高性能,在ViewPager+RecyclerView的應(yīng)用場景下可以大有作為。以上就是關(guān)于RecyclerView緩存的所有內(nèi)容,另外要備注一下,就是文章的圖片上有些單詞打錯了,實(shí)在是懶得重畫了,以文本的內(nèi)容為準(zhǔn),請大家見諒。
最后是github的地址:RecyclerViewCacheDemo






