在前一篇文章 RecyclerView 源碼分析(一) —— 繪製流程解析 介紹了 RecyclerView 的繪製流程,RecyclerView 經過將繪製流程從 View 中抽取出來,放到 LayoutManager 中,使得 RecyclerView 在不一樣的 LayoutManager 中,擁有不一樣的樣式,使得 RecyclerView 異常靈活,大大增強了 RecyclerView 使用場景。html
固然,RecyclerView 的緩存機制也是它特有的一個優勢,減小了對內存的佔用以及重複的繪製工做,所以,本文意在介紹和學習 RecyclerView 的緩存設計思想。數組
當咱們在討論混存的時候,必定會經歷建立-緩存-複用的過程。所以對於 RecyclerView 的緩存機制也是按照以下的步驟進行。緩存
在講到對子 itemView 測量的時候,layoutChunk 方法中會先得到每個 itemView,在獲取後,在將其添加到 RecyclerView 中。因此咱們先來看看建立的過程:微信
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
方法。數據結構
這個方法很長,可是其實邏輯很簡單,整個過程前面部分是先從緩存嘗試獲取 VH,若是找不到,就會建立新的 VH,而後綁定數據,最後將再將 VH 綁定到 LayoutParams (LP) 上。架構
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; // 省略從緩存查找 VH 的邏輯,下面是若是仍是沒找到,就會建立一個新的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; }
// 建立 VH 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); } } long end = getNanoTime(); mRecyclerPool.factorInCreateTime(type, end - start); if (DEBUG) { Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder"); } } } // This is very ugly but the only place we can grab this information // before the View is rebound and returned to the LayoutManager for post layout ops. // We don't need this in pre-layout since the VH is not updated by the LM. if (fromScrapOrHiddenOrCache && !mState.isPreLayout() && holder .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) { holder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST); if (mState.mRunSimpleAnimations) { int changeFlags = ItemAnimator .buildAdapterChangeFlagsForAnimations(holder); changeFlags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT; final ItemHolderInfo info = mItemAnimator.recordPreLayoutInformation(mState, holder, changeFlags, holder.getUnmodifiedPayloads()); recordAnimationInfoIfBouncedHiddenView(holder, info); } } 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); } final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams(); final LayoutParams rvLayoutParams;
// 下面邏輯就是將 VH 綁定到 LP, LP 又設置到 ItemView 上 if (lp == null) { rvLayoutParams = (LayoutParams) generateDefaultLayoutParams(); holder.itemView.setLayoutParams(rvLayoutParams); } else if (!checkLayoutParams(lp)) { rvLayoutParams = (LayoutParams) generateLayoutParams(lp); holder.itemView.setLayoutParams(rvLayoutParams); } else { rvLayoutParams = (LayoutParams) lp; } rvLayoutParams.mViewHolder = holder; rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound; return holder; }
即便省略了中間從緩存查找 VH 的邏輯,剩下部分的代碼仍是很長。那我再歸納下 tryGetViewHolderForPositionByDeadline 方法所作的事:app
從緩存查找 VH ;less
緩存沒有,那麼就建立一個 VH;函數
判斷 VH 需不須要更新數據,若是須要就會調用 tryBindViewHolderByDeadline 綁定數據;源碼分析
將 VH 綁定到 LP, LP 又設置到 ItemView 上,互相依賴;
到這裏關於建立 VH 的邏輯就講完了。
在介紹添加到緩存的邏輯時,仍是須要介紹緩存相關的類和變量。
Recycler 是 RecyclerView 的一個內部類。咱們來看一下它的主要的成員變量。
mAttachedScrap 緩存屏幕中可見範圍的 ViewHolder
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
mChangedScrap 緩存滑動時即將與 RecyclerView 分離的ViewHolder,按子View的position或id緩存,默認最多存放2個
ArrayList<ViewHolder> mChangedScrap = null;
mCachedViews ViewHolder 緩存列表,其大小由 mViewCacheMax 決定,默認 DEFAULT_CACHE_SIZE 爲 2,可動態設置。
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
ViewCacheExtension 開發者可自定義的一層緩存,是虛擬類 ViewCacheExtension 的一個實例,開發者可實現方法 getViewForPositionAndType(Recycler recycler, int position, int type) 來實現本身的緩存。
private ViewCacheExtension mViewCacheExtension;
RecycledViewPool ViewHolder 緩存池,在有限的 mCachedViews 中若是存不下 ViewHolder 時,就會把 ViewHolder 存入 RecyclerViewPool 中。
RecycledViewPool mRecyclerPool;
VH 被建立以後,是要被緩存,而後重複利用的,那麼他們是何時被添加到緩存的呢?此處仍是以 LinearLayoutManager 舉例說明。在 RecyclerView 源碼分析(一) —— 繪製流程解析 一文中曾提到一個方法:
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { // ... detachAndScrapAttachedViews(recycler); // ... }
onLayoutChildren 是對子 view 進行繪製。在對子 view 會先調用 detachAndScrapAttachedViews 方法,下面來看看這個方法。
下面來看下這個方法:
// recyclerview public void detachAndScrapAttachedViews(@NonNull Recycler recycler) { final int childCount = getChildCount(); for (int i = childCount - 1; i >= 0; i--) { final View v = getChildAt(i);
// 每一個 view 都會放到裏面 scrapOrRecycleView(recycler, i, v); } } private void scrapOrRecycleView(Recycler recycler, int index, View view) { final ViewHolder viewHolder = getChildViewHolderInt(view); if (viewHolder.shouldIgnore()) { if (DEBUG) { Log.d(TAG, "ignoring view " + viewHolder); } return; }
// 若是 VH 無效,而且已經被移除了,就會走另外一個邏輯 if (viewHolder.isInvalid() && !viewHolder.isRemoved() && !mRecyclerView.mAdapter.hasStableIds()) { removeViewAt(index); recycler.recycleViewHolderInternal(viewHolder); } else {
// 先 detch 掉,而後放入緩存中 detachViewAt(index); recycler.scrapView(view); mRecyclerView.mViewInfoStore.onViewDetached(viewHolder); } }
也就是在上面的邏輯裏,被放到緩存中。這裏就能夠看到
若是是 remove,會執行 recycleViewHolderInternal(viewHolder)
方法,而這個方法最終會將 ViewHolder 加入 CacheView 和 Pool 中,
而當是 Detach,會將 View 加入到 ScrapViews 中
須要指出的一點是:須要區分兩個概念,Detach 和 Remove
detach: 在 ViewGroup 中的實現很簡單,只是將 ChildView 從 ParentView 的 ChildView 數組中移除,ChildView 的 mParent 設置爲 null, 能夠理解爲輕量級的臨時 remove, 由於 View此時和 View 樹仍是藕斷絲連, 這個函數被常常用來改變 ChildView 在 ChildView 數組中的次序。View 被 detach 通常是臨時的,在後面會被從新 attach。
remove: 真正的移除,不光被從 ChildView 數組中除名,其餘和 View 樹各項聯繫也會被完全斬斷(不考慮 Animation/LayoutTransition 這種特殊狀況), 好比焦點被清除,從TouchTarget 中被移除等。
下面來看 Recycler 兩個的具體邏輯方法:
/** * internal implementation checks if view is scrapped or attached and throws an exception * if so. * Public version un-scraps before calling recycle. */ void recycleViewHolderInternal(ViewHolder holder) {
// ...省略前面的代碼,前面都是在作檢驗 final boolean transientStatePreventsRecycling = holder .doesTransientStatePreventRecycling(); @SuppressWarnings("unchecked") final boolean forceRecycle = mAdapter != null && transientStatePreventsRecycling && mAdapter.onFailedToRecycleView(holder); boolean cached = false; boolean recycled = false; if (DEBUG && mCachedViews.contains(holder)) { throw new IllegalArgumentException("cached view received recycle internal? " + holder + exceptionLabel()); } if (forceRecycle || holder.isRecyclable()) { if (mViewCacheMax > 0 && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) { // Retire oldest cached view 若是緩存數量超了,就會先移除最早加入的 int cachedViewSize = mCachedViews.size(); if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) { recycleCachedViewAt(0); cachedViewSize--; } int targetCacheIndex = cachedViewSize; if (ALLOW_THREAD_GAP_WORK && cachedViewSize > 0 && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) { // when adding the view, skip past most recently prefetched views int cacheIndex = cachedViewSize - 1; while (cacheIndex >= 0) { int cachedPos = mCachedViews.get(cacheIndex).mPosition; if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) { break; } cacheIndex--; } targetCacheIndex = cacheIndex + 1; }
// 添加到緩存 mCachedViews.add(targetCacheIndex, holder); cached = true; } if (!cached) { addViewHolderToRecycledViewPool(holder, true); recycled = true; } } else { } // even if the holder is not removed, we still call this method so that it is removed // from view holder lists. mViewInfoStore.removeViewHolder(holder); if (!cached && !recycled && transientStatePreventsRecycling) { holder.mOwnerRecyclerView = null; } }
該方法所作的事具體以下:
檢驗該 VH 的有效性,確保已經再也不被使用;
判斷緩存的容量,超了就會進行移除,而後找一個合適的位置進行添加。
mCachedViews 對應的數據結構也是 ArrayList 可是該緩存對集合的大小是有限制的,默認是 2。該緩存中 ViewHolder 的特性和 mAttachedScrap 中的特性是同樣的,只要 position或者 itemId 對應上了,那麼它就是乾淨的,無需從新綁定數據。開發者能夠調用 setItemViewCacheSize(size) 方法來改變緩存的大小。該層級緩存觸發的一個常見的場景是滑動 RV。固然 notifyXXX 也會觸發該緩存。該緩存和 mAttachedScrap 同樣特別高效。
RecyclerViewPool 緩存能夠針對多ItemType,設置緩存大小。默認每一個 ItemType 的緩存個數是 5。並且該緩存能夠給多個 RecyclerView 共享。因爲默認緩存個數爲 5,假設某個新聞 App,每屏幕能夠展現 10 條新聞,那麼必然會致使緩存命中失敗,頻繁致使建立 ViewHolder 影響性能。因此須要擴大緩存size。
接下去看 scrapView 這個方法:
void scrapView(View view) { final ViewHolder holder = getChildViewHolderInt(view); if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID) || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) { if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) { throw new IllegalArgumentException("Called scrap view with an invalid view." + " Invalid views cannot be reused from scrap, they should rebound from" + " recycler pool." + exceptionLabel()); } holder.setScrapContainer(this, false); // 這裏的 false mAttachedScrap.add(holder); } else { if (mChangedScrap == null) { mChangedScrap = new ArrayList<ViewHolder>(); } holder.setScrapContainer(this, true); // 這裏是 true mChangedScrap.add(holder); } }
該方法就比較簡單了,沒有那麼多須要檢驗的邏輯。這裏根據條件,有兩種緩存類型能夠選擇,具體就不展開了,你們均可以看懂。這裏講解下兩個 scrapView 的緩存。
mAttachedScrap 的對應數據結構是ArrayList,在 LayoutManager#onLayoutChildren 方法中,對 views 進行佈局時,會將 RecyclerView 上的 Views 所有暫存到該集合中,以備後續使用,該緩存中的 ViewHolder 的特性是,若是和 RV 上的 position 或者 itemId 匹配上了,那麼認爲是乾淨的 ViewHolder,是能夠直接拿出來使用的,無需調用 onBindViewHolder 方法。該 ArrayList 的大小是沒有限制的,屏幕上有多少個 View,就會建立多大的集合。
觸發該層級緩存的場景通常是調用 notifyItemXXX 方法。調用 notifyDataSetChanged 方法,只有當 Adapter hasStableIds 返回 true,會觸發該層級的緩存使用。
mChangedScrap 和 mAttachedScrap 是同一級的緩存,他們是平等的。可是mChangedScrap的調用場景是notifyItemChanged和notifyItemRangeChanged,只有發生變化的ViewHolder纔會放入到 mChangedScrap 中。mChangedScrap緩存中的ViewHolder是須要調用onBindViewHolder方法從新綁定數據的。那麼此時就有個問題了,爲何同一級別的緩存須要設計兩個不一樣的緩存?
在 dispatchLayoutStep2 階段 LayoutManager onLayoutChildren方法中最終會調用 layoutForPredictiveAnimations 方法,把 mAttachedScrap 中剩餘的 ViewHolder 填充到屏幕上,因此他們的區別就是,mChangedScrap 中的 ViewHolder 在 RV 填充滿的狀況下,是不會強行填充到 RV 上的。那麼有辦法可讓發生改變的 ViewHolder 進入 mAttachedScrap 緩存嗎?固然能夠。調用 notifyItemChanged(int position, Object payload) 方法能夠,實現局部刷新功能,payload 不爲空,那麼發生改變的 ViewHolder 是會被分離到 mAttachedScrap 中的。
下面進入到最後一節,使用緩存。這個在以前繪製篇幅也有提到,下面直接看對應的方法:
//根據傳入的position獲取ViewHolder ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) { ---------省略---------- boolean fromScrapOrHiddenOrCache = false; ViewHolder holder = null; //預佈局 屬於特殊狀況 從mChangedScrap中獲取ViewHolder if (mState.isPreLayout()) { holder = getChangedScrapViewForPosition(position); fromScrapOrHiddenOrCache = holder != null; } if (holder == null) { //一、嘗試從mAttachedScrap中獲取ViewHolder,此時獲取的是屏幕中可見範圍中的ViewHolder //二、mAttachedScrap緩存中沒有的話,繼續從mCachedViews嘗試獲取ViewHolder holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun); ----------省略---------- } if (holder == null) { final int offsetPosition = mAdapterHelper.findPositionOffset(position); ---------省略---------- final int type = mAdapter.getItemViewType(offsetPosition); //若是Adapter中聲明瞭Id,嘗試從id中獲取,這裏不屬於緩存 if (mAdapter.hasStableIds()) { holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun); } if (holder == null && mViewCacheExtension != null) { 3、從自定義緩存mViewCacheExtension中嘗試獲取ViewHolder,該緩存須要開發者實現 final View view = mViewCacheExtension .getViewForPositionAndType(this, position, type); if (view != null) { holder = getChildViewHolder(view); } } if (holder == null) { // fallback to pool //四、從緩存池mRecyclerPool中嘗試獲取ViewHolder holder = getRecycledViewPool().getRecycledView(type); if (holder != null) { //若是獲取成功,會重置ViewHolder狀態,因此須要從新執行Adapter#onBindViewHolder綁定數據 holder.resetInternal(); if (FORCE_INVALIDATE_DISPLAY_LIST) { invalidateDisplayListInt(holder); } } } if (holder == null) { ---------省略---------- //五、若以上緩存中都沒有找到對應的ViewHolder,最終會調用Adapter中的onCreateViewHolder建立一個 holder = mAdapter.createViewHolder(RecyclerView.this, type); } } boolean bound = false; if (mState.isPreLayout() && holder.isBound()) { holder.mPreLayoutPosition = position; } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) { final int offsetPosition = mAdapterHelper.findPositionOffset(position); //六、若是須要綁定數據,會調用Adapter#onBindViewHolder來綁定數據 bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs); } ----------省略---------- return holder; }
上述邏輯用流程圖表示:
總結一下上述流程:經過 mAttachedScrap、mCachedViews 及 mViewCacheExtension 獲取的 ViewHolder 不須要從新建立佈局及綁定數據;經過緩存池 mRecyclerPool 獲取的 ViewHolder不須要從新建立佈局,可是須要從新綁定數據;若是上述緩存中都沒有獲取到目標 ViewHolder,那麼就會回調 Adapter#onCreateViewHolder 建立佈局,以及回調 Adapter#onBindViewHolder來綁定數據。
咱們已經知道 ViewCacheExtension 屬於第三級緩存,須要開發者自行實現,那麼 ViewCacheExtension 在什麼場景下使用?又是如何實現的呢?
首先咱們要明確一點,那就是 Recycler
自己已經設置了好幾級緩存了,爲何還要留個接口讓開發者去自行實現緩存呢?
關於這一點,來看看 Recycler
中的其餘緩存:
mAttachedScrap
用來處理可見屏幕的緩存;
mCachedViews
裏存儲的數據雖然是根據 position
來緩存,可是裏面的數據隨時可能會被替換的;
mRecyclerPool
裏按 viewType
去存儲 ArrayList< ViewHolder>
,因此 mRecyclerPool
並不能按 position
去存儲 ViewHolder
,並且從 mRecyclerPool
取出的 View
每次都要去走 Adapter#onBindViewHolder
去從新綁定數據。
假如我如今須要在一個特定的位置(好比 position=0 位置)一直展現某個 View,且裏面的內容是不變的,那麼最好的狀況就是在特定位置時,既不須要每次從新建立 View,也不須要每次都去從新綁定數據,上面的幾種緩存顯然都是不適用的,這種狀況該怎麼辦呢?能夠經過自定義緩存 ViewCacheExtension
實現上述需求。
結論援引自:Android ListView 與 RecyclerView 對比淺析--緩存機制
ListView和RecyclerView緩存機制基本一致:
mActiveViews 和 mAttachedScrap 功能類似,意義在於快速重用屏幕上可見的列表項ItemView,而不須要從新 createView 和 bindView;
mScrapView 和 mCachedViews + mReyclerViewPool功能類似,意義在於緩存離開屏幕的 ItemView,目的是讓即將進入屏幕的 ItemView 重用.
RecyclerView 的優點在於
mCacheViews 的使用,能夠作到屏幕外的列表項 ItemView 進入屏幕內時也無須bindView快速重用;
mRecyclerPool 能夠供多個 RecyclerView 共同使用,在特定場景下,如 viewpaper+ 多個列表頁下有優點.客觀來講,RecyclerView 在特定場景下對 ListView 的緩存機制作了補強和完善。
不一樣使用場景:列表頁展現界面,須要支持動畫,或者頻繁更新,局部刷新,建議使用 RecyclerView,更增強大完善,易擴展;其它狀況(如微信卡包列表頁)二者都OK,但ListView在使用上會更加方便,快捷。
https://www.jianshu.com/p/2b19e9bcda84
https://www.jianshu.com/p/6e6bf58b7f0d
https://www.jianshu.com/p/e1b257484961