一 前言
RecyclerView据官方的介绍,该控件用于在有限的窗口中展示大量数据集,其实这样功能的控件我们并不陌生,例如:ListView、GridView。RecyclerView可以用来代替传统的ListView,GridView,更加强大和灵活。RecyclerView的使用网上有非常多案例,这里就不多说了,我们今天主要来看看RecyclerView 的缓存机制。
二 缓存机制Recycler详解
Recycler是RecyclerView的一个内部类。我们来看一下它的主要的成员变量。
- mChangedScrap 表示数据已经改变的ewHolder列表
- mAttachedScrap 未与RecyclerView分离的ViewHolder列表
- mCachedViews ViewHolder缓存列表,其大小由mViewCacheMax决定,默认DEFAULT_CACHE_SIZE为2,可动态设置。
- mViewCacheExtension 开发者可自定义的一层缓存,是虚拟类ViewCacheExtension的一个实例,开发者可实现方法getViewForPositionAndType(Recycler recycler, int position, int type)来实现自己的缓存。
- mRecyclerPool ViewHolder缓存池,在有限的mCachedViews中如果存不下ViewHolder时,就会把ViewHolder存入RecyclerViewPool中。
我们来看一张RecyclerView 缓存机制的流程图,如下图
贴上源码,如下。我们根据流程图和源码来分析RecyclerView的缓存机制。
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行holder = getChangedScrapViewForPosition(position);
我们通过position匹配 mChangedScrap 获取holder缓存。
getChangedScrapViewForPosition(position)方法内部通过2种方法获取holder缓存。第一种通过mChangedScrap匹配 position获取holder缓存。第二种通过mChangedScrap匹配id获取holder缓存。源码如下。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行代码。holder = getScrapViewForPosition(position, INVALID_TYPE, dryRun);
通过position查找废弃的holder,我们来看一下getScrapViewForPosition方法内部实现,主要通过3种方法获取holder缓存。
第一种从mAttachedScrap中通过匹配position获取holder缓存。 第二种通过ChildHelper找到隐藏但是没有被移除的View,通过getChildViewHolderInt(view)方法获取holder缓存。 第三种从mCachedViews中通过匹配position获取holder缓存。 getScrapViewForPosition源码如下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行代码。holder = getScrapViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
通过id获取holder缓存,getScrapViewForId方法内部主要通过2种方法获取holder缓存。
第一种从mAttachedScrap中通过匹配id获取holder缓存。 第二种从mCachedViews中通过匹配id获取holder缓存。 getScrapViewForId方法源码如下。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。最后有什么理解不对的地方请大家多多指教。谢谢。