咱們知道,RecyclerView在大量數據時依然能夠絲滑般順暢的滑動,那它到底是怎麼實現的呢,而RecyclerView之因此好用得益於它優秀的緩存機制。緩存
咱們知道,RecyclerView自己是一個ViewGroup,所以在滑動時就避免不了添加或移除子View(子View經過RecyclerView#Adapter中的onCreateViewHolder建立),若是每次使用子View都要去從新建立,確定會影響滑動的流暢性,因此RecyclerView經過Recycler來緩存的是ViewHolder(內部包含子View),這樣在滑動時能夠複用子View,某些條件下還能夠複用子View綁定的數據。因此本質上來講,RecyclerView之因此可以實現順暢的滑動效果,是由於緩存機制,由於緩存減小了重複繪製View和綁定數據的時間,從而提升了滑動時的性能。數據結構
Recycler緩存ViewHolder對象有4個等級,優先級從高到底依次爲:ide
mAttachedScrap存儲的是當前屏幕中的ViewHolder,mAttachedScrap的對應數據結構是ArrayList,在調用LayoutManager#onLayoutChildren方法時對views進行佈局,此時會將RecyclerView上的Views所有暫存到該集合中,該緩存中的ViewHolder的特性是,若是和RV上的position或者itemId匹配上了那麼能夠直接拿來使用的,無需調用onBindViewHolder方法。佈局
mChangedScrap和mAttachedScrap屬於同一級別的緩存,不過mChangedScrap的調用場景是notifyItemChanged和notifyItemRangeChanged,只有發生變化的ViewHolder纔會放入到mChangedScrap中。mChangedScrap緩存中的ViewHolder是須要調用onBindViewHolder方法從新綁定數據的。性能
mCachedViews緩存滑動時即將與RecyclerView分離的ViewHolder,按子View的position或id緩存,默認最多存放2個。mCachedViews對應的數據結構是ArrayList,可是該緩存對集合的大小是有限制的。this
該緩存中ViewHolder的特性和mAttachedScrap中的特性是同樣的,只要position或者itemId對應就無需從新綁定數據。開發者能夠調用setItemViewCacheSize(size)方法來改變緩存的大小,該層級緩存觸發的一個常見的場景是滑動RecyclerView。固然調用notify()也會觸發該緩存。spa
ViewCacheExtension是須要開發者本身實現的緩存,基本上頁面上的全部數據均可以經過它進行實現。code
ViewHolder緩存池,本質上是一個SparseArray,其中key是ViewType(int類型),value存放的是 ArrayList< ViewHolder>,默認每一個ArrayList中最多存放5個ViewHolder。對象
緩存級別 | 涉及對象 | 說明 | 是否從新建立視圖View | 是否從新綁定數據 |
---|---|---|---|---|
一級緩存 | mAttachedScrap mChangedScrap | 緩存屏幕中可見範圍的ViewHolder | false | false |
二級緩存 | mCachedViews | 緩存滑動時即將與RecyclerView分離的ViewHolder,按子View的position或id緩存 | false | false |
三級緩存 | mViewCacheExtension | 開發者自行實現的緩存 | ||
四級緩存 | mRecyclerPool | ViewHolder緩存池,本質上是一個SparseArray,其中key是ViewType(int類型),value存放的是 ArrayList< ViewHolder>,默認每一個ArrayList中最多存放5個ViewHolder | false | true |
一般,RecyclerView滑動時會觸發onTouchEvent#onMove,回收及複用ViewHolder在這裏就會開始。咱們知道設置RecyclerView時須要設置LayoutManager,LayoutManager負責RecyclerView的佈局,包含對ItemView的獲取與複用。以LinearLayoutManager爲例,當RecyclerView從新佈局時會依次執行下面幾個方法:圖片
上述的整個調用鏈:onLayoutChildren()->fill()->layoutChunk()->next()->getViewForPosition(),getViewForPosition()便是是從RecyclerView的回收機制實現類Recycler中獲取合適的View。
RecyclerView對ViewHolder的複用是從LayoutState的next()方法開始的。LayoutManager在佈局itemView時,須要獲取一個ViewHolder對象,以下所示。
View next(RecyclerView.Recycler recycler) { if (mScrapList != null) { return nextViewFromScrapList(); } final View view = recycler.getViewForPosition(mCurrentPosition); mCurrentPosition += mItemDirection; return view; }
next方法調用RecyclerView的getViewForPosition方法來獲取一個View,而getViewForPosition方法最終會調用到RecyclerView的tryGetViewHolderForPositionByDeadline方法,而RecyclerView真正複用的核心就在這裏。
@Nullable ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) { ViewHolder holder = null; // 0) 若是它是改變的廢棄的ViewHolder,在scrap的mChangedScrap找 if (mState.isPreLayout()) { holder = getChangedScrapViewForPosition(position); fromScrapOrHiddenOrCache = holder != null; } // 1)根據position分別在scrap的mAttachedScrap、mChildHelper、mCachedViews中查找 if (holder == null) { holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun); } if (holder == null) { final int type = mAdapter.getItemViewType(offsetPosition); // 2)根據id在scrap的mAttachedScrap、mCachedViews中查找 if (mAdapter.hasStableIds()) { holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun); } if (holder == null && mViewCacheExtension != null) { //3)在ViewCacheExtension中查找,通常不用到,因此沒有緩存 final View view = mViewCacheExtension .getViewForPositionAndType(this, position, type); if (view != null) { holder = getChildViewHolder(view); } } //4)在RecycledViewPool中查找 holder = getRecycledViewPool().getRecycledView(type); if (holder != null) { holder.resetInternal(); if (FORCE_INVALIDATE_DISPLAY_LIST) { invalidateDisplayListInt(holder); } } } //5)到最後若是尚未找到複用的ViewHolder,則新建一個 holder = mAdapter.createViewHolder(RecyclerView.this, type); }
能夠看到,tryGetViewHolderForPositionByDeadline()方法分別去scrap、CacheView、ViewCacheExtension、RecycledViewPool中獲取ViewHolder,若是沒有則建立一個新的ViewHolder。
通常狀況下,當咱們調用adapter的notifyItemChanged()方法,數據發生變化時,item緩存在mChangedScrap中,後續拿到的ViewHolder須要從新綁定數據。此時查找ViewHolder就會經過position和id分別在scrap的mChangedScrap中查找。
ViewHolder getChangedScrapViewForPosition(int position) { //經過position for (int i = 0; i < changedScrapSize; i++) { final ViewHolder holder = mChangedScrap.get(i); return holder; } // 經過id if (mAdapter.hasStableIds()) { final long id = mAdapter.getItemId(offsetPosition); for (int i = 0; i < changedScrapSize; i++) { final ViewHolder holder = mChangedScrap.get(i); return holder; } } return null; }
若是沒有找到視圖,根據position分別在scrap的mAttachedScrap、mChildHelper、mCachedViews中查找,涉及的方法以下。
ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) { final int scrapCount = mAttachedScrap.size(); // 首先從mAttachedScrap中查找,精準匹配有效的ViewHolder for (int i = 0; i < scrapCount; i++) { final ViewHolder holder = mAttachedScrap.get(i); return holder; } //接着在mChildHelper中mHiddenViews查找隱藏的ViewHolder if (!dryRun) { View view = mChildHelper.findHiddenNonRemovedView(position); if (view != null) { final ViewHolder vh = getChildViewHolderInt(view); scrapView(view); return vh; } } //最後從咱們的一級緩存中mCachedViews查找。 final int cacheSize = mCachedViews.size(); for (int i = 0; i < cacheSize; i++) { final ViewHolder holder = mCachedViews.get(i); return holder; } }
能夠看到,getScrapOrHiddenOrCachedHolderForPosition查找ViewHolder的順序以下:
若是在getScrapOrHiddenOrCachedHolderForPosition沒有找到視圖,澤經過id在scrap的mAttachedScrap、mCachedViews中查找,代碼以下。
ViewHolder getScrapOrCachedViewForId(long id, int type, boolean dryRun) { //在Scrap的mAttachedScrap中查找 final int count = mAttachedScrap.size(); for (int i = count - 1; i >= 0; i--) { final ViewHolder holder = mAttachedScrap.get(i); return holder; } //在一級緩存mCachedViews中查找 final int cacheSize = mCachedViews.size(); for (int i = cacheSize - 1; i >= 0; i--) { final ViewHolder holder = mCachedViews.get(i); return holder; } }
getScrapOrCachedViewForId()方法查找的順序以下:
mViewCacheExtension是由開發者定義的一層緩存策略,Recycler並無將任何view緩存到這裏。
if (holder == null && mViewCacheExtension != null) { final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type); if (view != null) { holder = getChildViewHolder(view); } }
這裏沒有自定義緩存策略,那麼就找不到對應的view。
在ViewHolder的四級緩存中,咱們有提到過RecycledViewPool,它是經過itemType把ViewHolder的List緩存到SparseArray中的,在getRecycledViewPool().getRecycledView(type)根據itemType從SparseArray獲取ScrapData ,而後再從裏面獲取ArrayList<ViewHolder>,從而獲取到ViewHolder。
@Nullable public ViewHolder getRecycledView(int viewType) { final ScrapData scrapData = mScrap.get(viewType);//根據viewType獲取對應的ScrapData if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) { final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap; for (int i = scrapHeap.size() - 1; i >= 0; i--) { if (!scrapHeap.get(i).isAttachedToTransitionOverlay()) { return scrapHeap.remove(i); } } } return null; }
若是尚未獲取到ViewHolder,則經過mAdapter.createViewHolder()建立一個新的ViewHolder返回。
// 若是尚未找到複用的ViewHolder,則新建一個 holder = mAdapter.createViewHolder(RecyclerView.this, type);
下面是尋找ViewHolder的一個完整的流程圖:
RecyclerView回收的入口有不少, 可是無論怎麼樣操做,RecyclerView 的回收或者複用必然涉及到add View 和 remove View 操做, 因此咱們從onLayout的流程入手分析回收和複用的機制。
首先,在LinearLayoutManager中,咱們來到itemView佈局入口的方法onLayoutChildren(),以下所示。
@Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { if (mPendingSavedState != null || mPendingScrollPosition != RecyclerView.NO_POSITION) { if (state.getItemCount() == 0) { removeAndRecycleAllViews(recycler);//移除全部子View return; } } ensureLayoutState(); mLayoutState.mRecycle = false;//禁止回收 //顛倒繪製佈局 resolveShouldLayoutReverse(); onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection); //暫時分離已經附加的view,即將全部child detach並經過Scrap回收 detachAndScrapAttachedViews(recycler); }
在onLayoutChildren()佈局的時候,先根據實際狀況是否須要removeAndRecycleAllViews()移除全部的子View,哪些ViewHolder不可用;而後經過detachAndScrapAttachedViews()暫時分離已經附加的ItemView,並緩存到List中。
detachAndScrapAttachedViews()的做用就是把當前屏幕全部的item與屏幕分離,將他們從RecyclerView的佈局中拿下來,保存到list中,在從新佈局時,再將ViewHolder從新一個個放到新的位置上去。
將屏幕上的ViewHolder從RecyclerView的佈局中拿下來後,存放在Scrap中,Scrap包括mAttachedScrap和mChangedScrap,它們是一個list,用來保存從RecyclerView佈局中拿下來ViewHolder列表,detachAndScrapAttachedViews()只會在onLayoutChildren()中調用,只有在佈局的時候,纔會把ViewHolder detach掉,而後再add進來從新佈局,可是你們須要注意,Scrap只是保存從RecyclerView佈局中當前屏幕顯示的item的ViewHolder,不參與回收複用,單純是爲了現從RecyclerView中拿下來再從新佈局上去。對於沒有保存到的item,會放到mCachedViews或者RecycledViewPool緩存中參與回收複用。
public void detachAndScrapAttachedViews(@NonNull Recycler recycler) { final int childCount = getChildCount(); for (int i = childCount - 1; i >= 0; i--) { final View v = getChildAt(i); scrapOrRecycleView(recycler, i, v); } }
上面代碼的做用是,遍歷全部view,分離全部已經添加到RecyclerView的itemView。
private void scrapOrRecycleView(Recycler recycler, int index, View view) { final ViewHolder viewHolder = getChildViewHolderInt(view); if (viewHolder.isInvalid() && !viewHolder.isRemoved() && !mRecyclerView.mAdapter.hasStableIds()) { removeViewAt(index);//移除VIew recycler.recycleViewHolderInternal(viewHolder);//緩存到CacheView或者RecycledViewPool中 } else { detachViewAt(index);//分離View recycler.scrapView(view);//scrap緩存 mRecyclerView.mViewInfoStore.onViewDetached(viewHolder); } }
而後,咱們看detachViewAt()方法分離視圖,再經過scrapView()緩存到scrap中。
void scrapView(View view) { final ViewHolder holder = getChildViewHolderInt(view); if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID) || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) { holder.setScrapContainer(this, false); mAttachedScrap.add(holder);//保存到mAttachedScrap中 } else { if (mChangedScrap == null) { mChangedScrap = new ArrayList<ViewHolder>(); } holder.setScrapContainer(this, true); mChangedScrap.add(holder);//保存到mChangedScrap中 } }
而後,咱們回到scrapOrRecycleView()方法中,進入if()分支。若是viewHolder是無效、未被移除、未被標記的則放到recycleViewHolderInternal()緩存起來,同時removeViewAt()移除了viewHolder。
void recycleViewHolderInternal(ViewHolder holder) { ····· if (forceRecycle || holder.isRecyclable()) { if (mViewCacheMax > 0 && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) { int cachedViewSize = mCachedViews.size(); if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {//若是超出容量限制,把第一個移除 recycleCachedViewAt(0); cachedViewSize--; } ····· mCachedViews.add(targetCacheIndex, holder);//mCachedViews回收 cached = true; } if (!cached) { addViewHolderToRecycledViewPool(holder, true);//放到RecycledViewPool回收 recycled = true; } } }
若是符合條件,會優先緩存到mCachedViews中時,若是超出了mCachedViews的最大限制,經過recycleCachedViewAt()將CacheView緩存的第一個數據添加到終極回收池RecycledViewPool後再移除掉,最後纔會add()新的ViewHolder添加到mCachedViews中。
剩下不符合條件的則經過addViewHolderToRecycledViewPool()緩存到RecycledViewPool中。
void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) { clearNestedRecyclerViewIfNotNested(holder); View itemView = holder.itemView; ······ holder.mOwnerRecyclerView = null; getRecycledViewPool().putRecycledView(holder);//將holder添加到RecycledViewPool中 }
最後,就是在填充佈局調用fill()方法的時候,它會回收移出屏幕的view到mCachedViews或者RecycledViewPool中。
int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) { if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) { recycleByLayoutState(recycler, layoutState);//回收移出屏幕的view } }
而recycleByLayoutState()方法就是用來回收移出屏幕的view,完整的流程以下圖。