RecyclerView 緩存機制詳解

一 前言

RecyclerView據官方的介紹,該控件用於在有限的窗口中展現大量數據集,其實這樣功能的控件咱們並不陌生,例如:ListView、GridView。RecyclerView能夠用來代替傳統的ListView,GridView,更增強大和靈活。RecyclerView的使用網上有很是多案例,這裏就很少說了,咱們今天主要來看看RecyclerView 的緩存機制。java

二 緩存機制Recycler詳解

Recycler是RecyclerView的一個內部類。咱們來看一下它的主要的成員變量。緩存

  1. mChangedScrap 表示數據已經改變的ewHolder列表
  2. mAttachedScrap 未與RecyclerView分離的ViewHolder列表
  3. mCachedViews ViewHolder緩存列表,其大小由mViewCacheMax決定,默認DEFAULT_CACHE_SIZE爲2,可動態設置。
  4. mViewCacheExtension 開發者可自定義的一層緩存,是虛擬類ViewCacheExtension的一個實例,開發者可實現方法getViewForPositionAndType(Recycler recycler, int position, int type)來實現本身的緩存。
  5. mRecyclerPool ViewHolder緩存池,在有限的mCachedViews中若是存不下ViewHolder時,就會把ViewHolder存入RecyclerViewPool中。

咱們來看一張RecyclerView 緩存機制的流程圖,以下圖
這裏寫圖片描述app

貼上源碼,以下。咱們根據流程圖和源碼來分析RecyclerView的緩存機制。less

public View getViewForPosition(int position) { return getViewForPosition(position, false); } View getViewForPosition(int position, boolean dryRun) { if (position < 0 || position >= mState.getItemCount()) { throw new IndexOutOfBoundsException("Invalid item position " + position + "(" + position + "). Item count:" + mState.getItemCount()); } boolean fromScrap = false; ViewHolder holder = null; // 0) If there is a changed scrap, try to find from there if (mState.isPreLayout()) { holder = getChangedScrapViewForPosition(position); fromScrap = holder != null; } // 1) Find from scrap by position if (holder == null) { holder = getScrapViewForPosition(position, INVALID_TYPE, dryRun); if (holder != null) { if (!validateViewHolderForOffsetPosition(holder)) { // recycle this scrap if (!dryRun) { // we would like to recycle this but need to make sure it is not used by // animation logic etc. holder.addFlags(ViewHolder.FLAG_INVALID); if (holder.isScrap()) { removeDetachedView(holder.itemView, false); holder.unScrap(); } else if (holder.wasReturnedFromScrap()) { holder.clearReturnedFromScrapFlag(); } recycleViewHolderInternal(holder); } holder = null; } else { fromScrap = true; } } } if (holder == null) { final int offsetPosition = mAdapterHelper.findPositionOffset(position); if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) { throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item " + "position " + position + "(offset:" + offsetPosition + ")." + "state:" + mState.getItemCount()); } final int type = mAdapter.getItemViewType(offsetPosition); // 2) Find from scrap via stable ids, if exists if (mAdapter.hasStableIds()) { holder = getScrapViewForId(mAdapter.getItemId(offsetPosition), type, dryRun); if (holder != null) { // update position holder.mPosition = offsetPosition; fromScrap = 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) { throw new IllegalArgumentException("getViewForPositionAndType returned" + " a view which does not have a ViewHolder"); } else if (holder.shouldIgnore()) { throw new IllegalArgumentException("getViewForPositionAndType returned" + " a view that is ignored. You must call stopIgnoring before" + " returning this view."); } } } if (holder == null) { // fallback to recycler // try recycler. // Head to the shared pool. if (DEBUG) { Log.d(TAG, "getViewForPosition(" + position + ") fetching from shared " + "pool"); } holder = getRecycledViewPool().getRecycledView(type); if (holder != null) { holder.resetInternal(); if (FORCE_INVALIDATE_DISPLAY_LIST) { invalidateDisplayListInt(holder); } } } if (holder == null) { holder = mAdapter.createViewHolder(RecyclerView.this, type); if (DEBUG) { Log.d(TAG, "getViewForPosition 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 (fromScrap && !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); } final int offsetPosition = mAdapterHelper.findPositionOffset(position); holder.mOwnerRecyclerView = RecyclerView.this; mAdapter.bindViewHolder(holder, offsetPosition); attachAccessibilityDelegate(holder.itemView); bound = true; if (mState.isPreLayout()) { holder.mPreLayoutPosition = position; } } final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams(); final LayoutParams rvLayoutParams; 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 = fromScrap && bound; return holder.itemView; }

 

主流程 1
咱們來看主流程源碼的第14行 ide

holder = getChangedScrapViewForPosition(position);

 

咱們經過position匹配 mChangedScrap 獲取holder緩存。
getChangedScrapViewForPosition(position)方法內部經過2種方法獲取holder緩存。第一種經過mChangedScrap匹配 position獲取holder緩存。第二種經過mChangedScrap匹配id獲取holder緩存。源碼以下。post

ViewHolder getChangedScrapViewForPosition(int position) { // If pre-layout, check the changed scrap for an exact match. final int changedScrapSize; if (mChangedScrap == null || (changedScrapSize = mChangedScrap.size()) == 0) { return null; } // 第一種 經過 position來查找 for (int i = 0; i < changedScrapSize; i++) { final ViewHolder holder = mChangedScrap.get(i); if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position) { holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP); return holder; } } //第二種 經過 id來查找 if (mAdapter.hasStableIds()) { final int offsetPosition = mAdapterHelper.findPositionOffset(position); if (offsetPosition > 0 && offsetPosition < mAdapter.getItemCount()) { final long id = mAdapter.getItemId(offsetPosition); for (int i = 0; i < changedScrapSize; i++) { final ViewHolder holder = mChangedScrap.get(i); if (!holder.wasReturnedFromScrap() && holder.getItemId() == id) { holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP); return holder; } } } } return null; }

 

主流程 2
咱們看一下主流程第19行代碼。fetch

holder = getScrapViewForPosition(position, INVALID_TYPE, dryRun);

 

經過position查找廢棄的holder,咱們來看一下getScrapViewForPosition方法內部實現,主要經過3種方法獲取holder緩存。
第一種從mAttachedScrap中經過匹配position獲取holder緩存。
第二種經過ChildHelper找到隱藏可是沒有被移除的View,經過getChildViewHolderInt(view)方法獲取holder緩存。
第三種從mCachedViews中經過匹配position獲取holder緩存。
getScrapViewForPosition源碼以下ui

ViewHolder getScrapViewForPosition(int position, int type, boolean dryRun) { final int scrapCount = mAttachedScrap.size(); // 第一種從mAttachedScrap中經過匹配position獲取holder緩存。 for (int i = 0; i < scrapCount; i++) { final ViewHolder holder = mAttachedScrap.get(i); if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position && !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) { if (type != INVALID_TYPE && holder.getItemViewType() != type) { Log.e(TAG, "Scrap view for position " + position + " isn't dirty but has" + " wrong view type! (found " + holder.getItemViewType() + " but expected " + type + ")"); break; } holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP); return holder; } } //經過ChildHelper找到隱藏可是沒有被移除的View,經過getChildViewHolderInt(view)方法獲取holder緩存。 if (!dryRun) { View view = mChildHelper.findHiddenNonRemovedView(position, type); if (view != null) { // This View is good to be used. We just need to unhide, detach and move to the // scrap list. final ViewHolder vh = getChildViewHolderInt(view); mChildHelper.unhide(view); int layoutIndex = mChildHelper.indexOfChild(view); if (layoutIndex == RecyclerView.NO_POSITION) { throw new IllegalStateException("layout index should not be -1 after " + "unhiding a view:" + vh); } mChildHelper.detachViewFromParent(layoutIndex); scrapView(view); vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP | ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST); return vh; } } // Search in our first-level recycled view cache. //第三種從mCachedViews中經過匹配position獲取holder緩存。 final int cacheSize = mCachedViews.size(); for (int i = 0; i < cacheSize; i++) { final ViewHolder holder = mCachedViews.get(i); // invalid view holders may be in cache if adapter has stable ids as they can be // retrieved via getScrapViewForId if (!holder.isInvalid() && holder.getLayoutPosition() == position) { if (!dryRun) { mCachedViews.remove(i); } if (DEBUG) { Log.d(TAG, "getScrapViewForPosition(" + position + ", " + type + ") found match in cache: " + holder); } return holder; } } return null; }

 

主流程 3
咱們看一下主流程第52行代碼。this

holder = getScrapViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);

 

經過id獲取holder緩存,getScrapViewForId方法內部主要經過2種方法獲取holder緩存。
第一種從mAttachedScrap中經過匹配id獲取holder緩存。
第二種從mCachedViews中經過匹配id獲取holder緩存。
getScrapViewForId方法源碼以下。spa

ViewHolder getScrapViewForId(long id, int type, boolean dryRun) { //第一種從mAttachedScrap中經過匹配id獲取holder緩存。 // Look in our attached views first final int count = mAttachedScrap.size(); for (int i = count - 1; i >= 0; i--) { final ViewHolder holder = mAttachedScrap.get(i); if (holder.getItemId() == id && !holder.wasReturnedFromScrap()) { if (type == holder.getItemViewType()) { holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP); if (holder.isRemoved()) { // this might be valid in two cases: // > item is removed but we are in pre-layout pass // >> do nothing. return as is. make sure we don't rebind // > item is removed then added to another position and we are in // post layout. // >> remove removed and invalid flags, add update flag to rebind // because item was invisible to us and we don't know what happened in // between. if (!mState.isPreLayout()) { holder.setFlags(ViewHolder.FLAG_UPDATE, ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED); } } return holder; } else if (!dryRun) { // if we are running animations, it is actually better to keep it in scrap // but this would force layout manager to lay it out which would be bad. // Recycle this scrap. Type mismatch. mAttachedScrap.remove(i); removeDetachedView(holder.itemView, false); quickRecycleScrapView(holder.itemView); } } } //第二種從mCachedViews中經過匹配id獲取holder緩存。 // Search the first-level cache final int cacheSize = mCachedViews.size(); for (int i = cacheSize - 1; i >= 0; i--) { final ViewHolder holder = mCachedViews.get(i); if (holder.getItemId() == id) { if (type == holder.getItemViewType()) { if (!dryRun) { mCachedViews.remove(i); } return holder; } else if (!dryRun) { recycleCachedViewAt(i); } } } return null; }

 

主流程 4
咱們看一下主流程第62行代碼。
經過mViewCacheExtension.getViewForPositionAndType獲取view,經過getChildViewHolder(view)獲取holder緩存。源碼以下

final View view = mViewCacheExtension
                        .getViewForPositionAndType(this, position, type);
                if (view != null) {
                    holder = getChildViewHolder(view);
                    if (holder == null) {
                        throw new IllegalArgumentException("getViewForPositionAndType returned"
                                + " a view which does not have a ViewHolder");
                    } else if (holder.shouldIgnore()) {
                        throw new IllegalArgumentException("getViewForPositionAndType returned"
                                + " a view that is ignored. You must call stopIgnoring before"
                                + " returning this view.");
                    }
                }

 

主流程 5
咱們看一下主流程第83行代碼。
holder = getRecycledViewPool().getRecycledView(type);
經過RecyclerView 的ViewHolder緩存池獲取holder。
經過holder.resetInternal();方法將holder復位,爲後續從新綁定作好準備。

主流程 6
咱們看一下主流程第92行代碼。
holder = mAdapter.createViewHolder(RecyclerView.this, type);建立新的holder

主流程 7
咱們看一下主流程第119行代碼。
if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid())
判斷是否要從新綁定ViewHolder。

主流程就是這樣了。

三 總結

通過上面的分析,咱們能夠看出RecyclerView 緩存機制(Recycler)大體能夠分爲5級。
第一級 經過mChangedScrap匹配 position或者id獲取holder緩存。
第二級 從mAttachedScrap中經過匹配position獲取holder緩存,或者經過ChildHelper找到隱藏可是沒有被移除的View,經過getChildViewHolderInt(view)方法獲取holder緩存,或者
從mCachedViews中經過匹配position獲取holder緩存。
第三級 從mAttachedScrap中經過匹配id獲取holder緩存,或者
從mCachedViews中經過匹配id獲取holder緩存。
第四級 從ViewCacheExtension獲取holder緩存。
第五級 經過RecyclerView 的ViewHolder緩存池獲取holder。

最後有什麼理解不對的地方請你們多多指教。謝謝。

相關文章
相關標籤/搜索