recyclerview item点击无效_让你彻底掌握RecyclerView的缓存机制
點擊上方藍字關注???
來源:肖邦kakahttps://www.jianshu.com/p/3e9aa4bdaefd前言
RecyclerView這個控件幾乎所有的Android開發者都使用過(甚至不用加幾乎),它是真的很好用,完美取代了ListView和GridView,而RecyclerView之所以好用,得益于它優秀的緩存機制。關于RecyclerView緩存機制,更是需要我們開發者來掌握的。本文就將先從整體流程看RecyclerView的緩存,再帶你從源碼角度分析,跳過讀源碼的坑,最后用一個簡單的demo的形式展示出來。在開始RecyclerView的緩存機制之前我們先學習關于ViewHolder的知識。
RecyclerView為什么強制我們實現ViewHolder模式?
關于這個問題,我們首先看一下ListView。ListView是不強制我們實現ViewHolder的,但是后來google建議我們實現ViewHolder模式。我們先分別看一下這兩種不同的方式。
其實這里我已經用紅框標出來了,ListView使用ViewHolder的好處就在于可以避免每次getView都進行findViewById()操作,因為findViewById()利用的是DFS算法(深度優化搜索),是非常耗性能的。而對于RecyclerView來說,強制實現ViewHolder的其中一個原因就是避免多次進行findViewById()的處理,另一個原因就是因為ItemView和ViewHolder的關系是一對一,也就是說一個ViewHolder對應一個ItemView。這個ViewHolder當中持有對應的ItemView的所有信息,比如說:position;view;width等等,拿到了ViewHolder基本就拿到了ItemView的所有信息,而ViewHolder使用起來相比itemView更加方便。RecyclerView緩存機制緩存的就是ViewHolder(ListView緩存的是ItemView),這也是為什么RecyclerView為什么強制我們實現ViewHolder的原因。
ListView的緩存機制
在正式講RecyclerView的緩存機制之前還需要提一嘴ListView的緩存機制,不多BB,先上圖:
ListView的緩存有兩級,在ListView里面有一個內部類 RecycleBin,RecycleBin有兩個對象Active View和Scrap View來管理緩存,Active View是第一級,Scrap View是第二級。
Active View:是緩存在屏幕內的ItemView,當列表數據發生變化時,屏幕內的數據可以直接拿來復用,無須進行數據綁定。
Scrap view:緩存屏幕外的ItemView,這里所有的緩存的數據都是"臟的",也就是數據需要重新綁定,也就是說屏幕外的所有數據在進入屏幕的時候都要走一遍getView()方法。再來一張圖,看看ListView的緩存流程
當Active View和Scrap View中都沒有緩存的時候就會直接create view。
小結
ListView的緩存機制相對比較好理解,它只有兩級緩存,一級緩存Active View是負責屏幕內的ItemView快速復用,而Scrap View是緩存屏幕外的數據,當該數據從屏幕外滑動到屏幕內的時候需要走一遍getView()方法。
RecyclerView的緩存機制
先上圖:
RecyclerView的緩存分為四級
Scrap
Cache
ViewCacheExtension
RecycledViewPool
Scrap對應ListView 的Active View,就是屏幕內的緩存數據,就是相當于換了個名字,可以直接拿來復用。
Cache 剛剛移出屏幕的緩存數據,默認大小是2個,當其容量被充滿同時又有新的數據添加的時候,會根據FIFO原則,把優先進入的緩存數據移出并放到下一級緩存中,然后再把新的數據添加進來。Cache里面的數據是干凈的,也就是攜帶了原來的ViewHolder的所有數據信息,數據可以直接來拿來復用。需要注意的是,cache是根據position來尋找數據的,這個postion是根據第一個或者最后一個可見的item的position以及用戶操作行為(上拉還是下拉)。舉個栗子:當前屏幕內第一個可見的item的position是1,用戶進行了一個下拉操作,那么當前預測的position就相當于(1-1=0),也就是position=0的那個item要被拉回到屏幕,此時RecyclerView就從Cache里面找position=0的數據,如果找到了就直接拿來復用。
ViewCacheExtension是google留給開發者自己來自定義緩存的,這個ViewCacheExtension我個人建議還是要慎用,因為我扒拉扒拉網上其他的博客,沒有找到對應的使用場景,而且這個類的api設計的也有些奇怪,只有一個public abstract View getViewForPositionAndType(@NonNull Recycler recycler, int position, int type);讓開發者重寫通過position和type拿到ViewHolder的方法,卻沒有提供如何產生ViewHolder或者管理ViewHolder的方法,給人一種只出不進的趕腳,還是那句話慎用。
RecycledViewPool剛才說了Cache默認的緩存數量是2個,當Cache緩存滿了以后會根據FIFO(先進先出)的規則把Cache先緩存進去的ViewHolder移出并緩存到RecycledViewPool中,RecycledViewPool默認的緩存數量是5個。RecycledViewPool與Cache相比不同的是,從Cache里面移出的ViewHolder再存入RecycledViewPool之前ViewHolder的數據會被全部重置,相當于一個新的ViewHolder,而且Cache是根據position來獲取ViewHolder,而RecycledViewPool是根據itemType獲取的,如果沒有重寫getItemType()方法,itemType就是默認的。因為RecycledViewPool緩存的ViewHolder是全新的,所以取出來的時候需要走onBindViewHolder()方法。再來張圖看看整體流程
這里大家先記住主要流程,并且記住各級緩存是根據什么拿到ViewHolder以及ViewHolder能否直接拿來復用,先有一個整體的認識,下面我會帶著大家再簡單分析一下RecyclerView緩存機制的源碼。
閱讀RecyclerView緩存機制源碼
由于篇幅和內容的關系,我不可能帶大家一行一行讀,這里我只列出關鍵點,還有哪些需要重點看,哪些可以直接略過,避免大家陷入讀源碼一個勁兒鉆進去出不來的誤區。當RecyclerView繪制的時候,會走到LayoutManager里面的next()方法,在next()里面是正式開始使用緩存機制,這里以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的內部類。我們先去看一眼這個類
public final class Recycler {final ArrayList mAttachedScrap = new ArrayList<>();
ArrayList mChangedScrap = null;final ArrayList mCachedViews = new ArrayList();private final List
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 mScrapHeap = new ArrayList<>();int mMaxScrap = DEFAULT_MAX_SCRAP;long mCreateRunningAverageNs = 0;long mBindRunningAverageNs = 0;
}
SparseArray mScrap = new SparseArray<>();
其中mAttachedScrap對應Scrap;mCachedViews對應Cache;mViewCacheExtension對應ViewCacheExtension;mRecyclerPool對應RecycledViewPool。注意:mAttachedScrap、mCachedViews和RecycledViewPool里面的mScrapHeap都是ArrayList,緩存被加入到這三個對象里面實際上就是調用的ArrayList.add()方法,復用緩存呢,這里要注意一下不是調用的ArrayList.get()而是ArrayList.remove(),其實這里也很好理解,因為當緩存數據被取出來展示到了屏幕內,自然就應該被移除。我們現在回到剛才的next()方法里,recycler.getViewForPosition(mCurrentPosition); 直接去看getViewForPosition這個方法,接著跟到了這里
View getViewForPosition(int position, boolean dryRun) {return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
接著跟進去
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 thereif (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}// 1) Find by position from scrap/hidden list/cacheif (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
}if (holder == null) {final int type = mAdapter.getItemViewType(offsetPosition);// 2) Find from scrap/cache via stable ids, if existsif (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 poolif (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 meetreturn 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;
}
終于到了緩存機制最核心的地方,為了方便大家閱讀,我對這部分源碼進行了刪減,直接從官方給的注釋里面看。
// (0) If there is a changed scrap, try to find from thereif (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
這里面只有設置動畫以后才會為true,跟咱們講的緩存也沒有多大關系,直接略過。
// 1) Find by position from scrap/hidden list/cacheif (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
}
這里就開始拿第一級和第二級緩存了getScrapOrHiddenOrCachedHolderForPosition()這個方法可以深入去看以下,注意這里傳的參數是position(dryRun這個參數不用管),就跟我之前說的,Scrap和Cache是根據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就不會進入判斷條件,還是那句話慎用。
if (holder == null) { // fallback to poolif (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 meetreturn 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了,創建一個新的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是不是有效的,也就是可不可以復用,如果不可以復用就會進入tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);這個方法,在這里面調用了bindViewHolder()方法。點進去看一眼
private boolean tryBindViewHolderByDeadline(@NonNull ViewHolder holder, int offsetPosition,
int position, long deadlineNs) {
....................mAdapter.bindViewHolder(holder, offsetPosition);
....................return true;
}
在點進去就到了我們熟悉的onBindViewHolder()
public final void bindViewHolder(@NonNull VH holder, int position) {
.......................onBindViewHolder(holder, position, holder.getUnmodifiedPayloads());
.........................
}
至此,緩存機制的整體流程就全部分析完畢了。
小結
ListView有兩級緩存,分別是Active View和Scrap View,緩存的對象是ItemView;而RecyclerView有四級緩存,分別是Scrap、Cache、ViewCacheExtension和RecycledViewPool,緩存的對象是ViewHolder。Scrap和Cache分別是通過position去找ViewHolder可以直接復用;ViewCacheExtension自定義緩存,目前來說應用場景比較少卻需慎用;RecycledViewPool通過type來獲取ViewHolder,獲取的ViewHolder是個全新,需要重新綁定數據。當你看到這里的時候,面試官再問RecyclerView的性能比ListView優化在哪里,我想你已經有答案。
通過demo理解
擔心你看完上面的內容,倒頭就忘,我們寫個簡單的demo通過打印log的方式來鞏固一下學到的知識。簡單說一下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;
}@Overrideprotected 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();
}
}
其實很簡單,在RecyclerView執行onLayout()方法前后執行一下咱們打印緩存變化的方法
再看一眼打印緩存變化的方法,利用反射的技術
/**
* 利用java反射機制拿到RecyclerView內的緩存并打印出來
* */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 mAttached =
(ArrayListWrapper) mAttachedScrap.get(recyclerInstance);
ArrayList mChanged =
(ArrayList) mChangedScrap.get(recyclerInstance);
ArrayList mCached =
(ArrayList) 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,在調onAttachedToWindow()方法的時候會進行版本判斷,如果是5.0以及以上的系統(即大于等于21),GapWorker會把RecyclerView自己加入到GapWorker。在RenderThread線程執行預取操作的時候會mPrefetchMaxCountObserved = 1,這就會導致你使用5.0以及以上系統的手機打印緩存數量的時候會比你預想的多一個。這里為了不造成這種問題,本文使用4.4系統的Android模擬器來演示Demo。
Demo演示效果截圖
啟動App,第一次加載的情況
初始化加載只有屏幕內的一級緩存7個
把position = 0 和position=1 兩個item移除屏幕
看藍色框出來的,position = 0 和position = 1的item被加入到了Cache緩存中,Cache的緩存數量我沒有修改,默認2個,也就說現在已經滿了
再把position = 2的item也移除屏幕
因為上一步Cache里面的緩存已經慢了,此時position = 2又被加入緩存,根據FIFO的原則,cache里面position = 0 被remove掉并加入到了四級緩存RecycledView里面,此時RecycledView也有了緩存并且該緩存沒有任何有效數據信息。
再上一步的基礎上下拉一下,把position = 2的item顯示出來
此時position = 2的item將要被顯示出來,會先從cache里面找,發現Cache正好有position = 2的緩存就直接拿出來復用了,并且原來在屏幕里的position= 9 的item被移除了,就會加入到Cache的緩存里。現在看一下onCreateViewHolder()和onBindViewHolder()的情況
還是啟動App,第一次加載后,再把position = 0和position =1的item移除屏幕再移回來
onBindViewHolder()方法沒有被重復執行(靜態圖顯示的效果不是很好,gif錄制的質量太差了,還是建議下載demo自己嘗試一下)
最后留一個問題給大家
為什么在第10次onCreateViewHolder()執行以后就再也沒有執行過onCreateViewHolder()方法了?
總結
關于RecyclerView的緩存分為四級,Scrap、Cache、ViewCacheExtension和RecycledViewPool。Scrap是屏幕內的緩存一般我們不怎么需要特別注意;Cache可直接拿來復用的緩存,性能高效;ViewCacheExtension需要開發者自定義的緩存,API設計比較奇怪,慎用;RecycledViewPool四級緩存,可以避免用戶調用onCreateViewHolder()方法,提高性能,在ViewPager+RecyclerView的應用場景下可以大有作為。以上就是關于RecyclerView緩存的所有內容,另外要備注一下,就是文章的圖片上有些單詞打錯了,實在是懶得重畫了,以文本的內容為準,請大家見諒。
最后是github的地址:
https://github.com/kaka10xiaobang/RecyclerViewCacheDemo
—————END—————
總結
以上是生活随笔為你收集整理的recyclerview item点击无效_让你彻底掌握RecyclerView的缓存机制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ppt批量缩略图_PPT如何在文件夹下显
- 下一篇: js对文字批注_实现SpreadJS的自