RecyclerView源码分析(二):RecyclerView的缓存与复用机制

2024-08-25 13:08

本文主要是介绍RecyclerView源码分析(二):RecyclerView的缓存与复用机制,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

1、开篇

在上一篇文章说到,LineaLayoutManager在填充布局之前会先调用detachAndScrapAttachedViews方法先暂时回收子View,然后在layoutChunk中进行子View的获取(可能会创建View)、测量、布局以及回收流程。实际上三大LayoutManager的流程都是一样的,只是细节上有所差异,感兴趣的同学可以阅读一下源码看看。本篇中涉及LayoutManager的部分依旧会以LinearLayoutManager为例。
回顾一下本系列要解决的问题:

  1. 既然是个ViewGroup,那少不了要问上一句:它的measure、layout和draw是怎么样的?
  2. RecyclerView是怎么回收View的?什么时候回收?
  3. 怎么支持多类型Item的?怎么缓存和查找的呢?
  4. Adapter的onRecreateViewHolder和onBindViewHolder两大核心方法是什么时候调用的?
  5. Item动画过程中notifyXXXChange会不会导致动画的错位?

问题1上篇文章已经解决了,而本篇要解决其中的234。

2、LayoutManager#detachAndScrapAttachedViews

detachAndScrapAttachedViews是基类LayoutManager定义的方法,如下所示:

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);}
}private void scrapOrRecycleView(Recycler recycler, int index, View view) {final ViewHolder viewHolder = getChildViewHolderInt(view);// shouldIgnore()为true表示ViewHolder不应该被回收if (viewHolder.shouldIgnore()) {if (DEBUG) {Log.d(TAG, "ignoring view " + viewHolder);}return;}// isInvalid()为true表示Item的数据发生变化了(notifyDataSetChanged),需要重新绑定// isRemoved()为true表示Item被移除了// hasStableIds()为true表示每个Item会拥有一个唯一的long类型的id// 我们可以通过Adapter.setHasStableIds(true)并重写Adapter.getItemId(int)方法来指定每个Item的idif (viewHolder.isInvalid() && !viewHolder.isRemoved()&& !mRecyclerView.mAdapter.hasStableIds()) {removeViewAt(index);recycler.recycleViewHolderInternal(viewHolder);} else {detachViewAt(index);recycler.scrapView(view);mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);}
}

detachAndScrapAttachedViews遍历了子View,挨个调用了scrapOrRecycleView方法进行回收。scrapOrRecycleView方法对不同情况的View进行了不同的回收处理:

  • viewHolder.shouldIgnore()为true:不回收
  • 数据发生变化,但是Item没有被移除且没有指定ItemId
  • 其他情况

2.1 数据发生变化,但是Item没有被移除且没有指定ItemId

不回收的情况就不用多说了,来看一下第二种情况。先看removeViewAt(index)方法

LayoutManager#removeViewAt:
public void removeViewAt(int index) {final View child = getChildAt(index);if (child != null) {mChildHelper.removeViewAt(index);}
}ChildHelper#removeViewAt:
void removeViewAt(int index) {final int offset = getOffset(index);final View view = mCallback.getChildAt(offset);if (view == null) {return;}if (mBucket.remove(offset)) {unhideViewInternal(view);}mCallback.removeViewAt(offset);if (DEBUG) {Log.d(TAG, "removeViewAt " + index + ", off:" + offset + ", " + this);}
}

可以看到,最终是调用了ChildHelper的mCallback.removeViewAt方法。这个mCallback是ChildHelper的构造方法传入的,它的实例化在RecyclerView#initChildrenHelper中:

private void initChildrenHelper() {mChildHelper = new ChildHelper(new ChildHelper.Callback() {...@Overridepublic void removeViewAt(int index) {final View child = RecyclerView.this.getChildAt(index);if (child != null) {dispatchChildDetached(child);// Clear any android.view.animation.Animation that may prevent the item from// detaching when being removed. If a child is re-added before the// lazy detach occurs, it will receive invalid attach/detach sequencing.// 清除View的动画child.clearAnimation();}RecyclerView.this.removeViewAt(index);}...});
}

所以在这种情况下,子View真的从RecyclerView中移除了。那么再来看Recycler的recycleViewHolderInternal方法对这个View已经被移除的ViewHolder做了些什么事情

void recycleViewHolderInternal(ViewHolder holder) {// 对holder进行各种状态检查,这段代码省略...// 强制回收判断// transientStatePreventsRecycling如果是true代表View处于某种临时状态(执行动画等)不能被回收// 这时候可以通过重写Adapter.onFailedToRecycleView方法来解除这个临时状态并返回true以达到可以被回收的目的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()) { // 强制回收或者holder是可回收的if (mViewCacheMax > 0&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID| ViewHolder.FLAG_REMOVED| ViewHolder.FLAG_UPDATE| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) { // 注意这里的flag,没有这些flag的才会走接下来的回收流程// 从这里可以看出mViewCacheMax是mCachedViews的存储数量上限// 如果达到了这个上限,先移除最老的int cachedViewSize = mCachedViews.size();if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {recycleCachedViewAt(0);cachedViewSize--;}int targetCacheIndex = cachedViewSize;... // 省略targetCacheIndex计算逻辑mCachedViews.add(targetCacheIndex, holder);cached = true;}if (!cached) {addViewHolderToRecycledViewPool(holder, true);recycled = true;}} else {// 注意:滚动时,如果View在执行动画的过程中可能会回收失败// 这种情况下,ItemAnimatorRestoreListener#onAnimationFinished回调中回收此View// 请考虑取消滚动出去时取消View的动画,以使View更快地被回收到回收池if (DEBUG) {Log.d(TAG, "trying to recycle a non-recycleable holder. Hopefully, it will "+ "re-visit here. We are still removing it from animation lists"+ exceptionLabel());}}// even if the holder is not removed, we still call this method so that it is removed// from view holder lists.// 即使holder没有被移除/回收,也从list中先行移除mViewInfoStore.removeViewHolder(holder);if (!cached && !recycled && transientStatePreventsRecycling) {holder.mOwnerRecyclerView = null;}
}void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {...if (dispatchRecycled) {dispatchViewRecycled(holder);}holder.mOwnerRecyclerView = null;getRecycledViewPool().putRecycledView(holder);
}

这里可以看出,mViewCacheMax<=0或者有以下几个flag之一的情况是不会进行holder的回收的

ViewHolder.FLAG_INVALID
ViewHolder.FLAG_REMOVED
ViewHolder.FLAG_UPDATE
ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN

回收的时候,会优先回收到mCachedViews中,mCachedViews是一个ViewHolder的ArrayList。而RecycledViewPool是一个比较简单的对象。注意,我们当前情况是有FLAG_INVALID这个flag的,所以就当前情况是不会回收到mCachedViews中的。

public static class RecycledViewPool {private static final int DEFAULT_MAX_SCRAP = 5;static class ScrapData {final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();int mMaxScrap = DEFAULT_MAX_SCRAP;long mCreateRunningAverageNs = 0;long mBindRunningAverageNs = 0;}SparseArray<ScrapData> mScrap = new SparseArray<>();public ViewHolder getRecycledView(int viewType) {final ScrapData scrapData = mScrap.get(viewType);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;}...public void putRecycledView(ViewHolder scrap) {final int viewType = scrap.getItemViewType();final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {return;}if (DEBUG && scrapHeap.contains(scrap)) {throw new IllegalArgumentException("this scrap item already exists");}// 重置了scrap.resetInternal();scrapHeap.add(scrap);}...
}

其中ScrapData简单封装了一个ArrayList,而RecycledViewPool封装了一个SparseArray,key是viewType,value是ScrapData。也就是说,在RecyclerViewPool里面,ViewHolder是按照viewType分门别类地存储的。

2.2 其他情况

其他情况调用如下:

LayoutManager#scrapOrRecycleView:
private void scrapOrRecycleView(Recycler recycler, int index, View view) {...else {detachViewAt(index);recycler.scrapView(view);mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);}
}

先看LayoutManager的detachViewAt

public void detachViewAt(int index) {detachViewInternal(index, getChildAt(index));
}private void detachViewInternal(int index, @NonNull View view) {if (DISPATCH_TEMP_DETACH) {ViewCompat.dispatchStartTemporaryDetach(view);}mChildHelper.detachViewFromParent(index);
}

mChildHelper最终其实也是调用了mCallback的相关回调


private void initChildrenHelper() {mChildHelper = new ChildHelper(new ChildHelper.Callback() {...@Overridepublic void detachViewFromParent(int offset) {final View view = getChildAt(offset);if (view != null) {final ViewHolder vh = getChildViewHolderInt(view);if (vh != null) {...// 加一个flag: FLAG_TMP_DETACHEDvh.addFlags(ViewHolder.FLAG_TMP_DETACHED);}}RecyclerView.this.detachViewFromParent(offset);}...});
}

最终调用了RecyclerView的detachViewFromParent。

再看Recycler的scrapView

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);} else {if (mChangedScrap == null) {mChangedScrap = new ArrayList<ViewHolder>();}holder.setScrapContainer(this, true);mChangedScrap.add(holder);}
}boolean canReuseUpdatedViewHolder(ViewHolder viewHolder) {return mItemAnimator == null || mItemAnimator.canReuseUpdatedViewHolder(viewHolder,viewHolder.getUnmodifiedPayloads());
}

被标记为具有FLAG_REMOVED或者FLAG_INVALID、或者没有更新的以及可重用的ViewHolder被添加到了mAttachedScrap中,其余的被添加到了mChangedScrap中。这两个变量都是存放ViewHolder的ArrayList。

2.4 总结

现在来总结一下LayoutManager#detachAndScrapAttachedViews中的主要工作:

  1. 如果ViewHolder.shouldIgnore()返回true,忽略
  2. 如果ViewHolder.isInvalid()返回true且没有被移除也没有有效的id的时候,优先考虑缓存到mCachedViews里,mCachedViews达到设定的上限后,老的ViewHolder缓存到RecycledViewPool
  3. 其余情况,被标记为具有FLAG_REMOVED或者FLAG_INVALID、或者没有更新的以及可重用的ViewHolder被添加到了mAttachedScrap中,其余的被添加到了mChangedScrap中

3、LayoutState#next

看一下LinearLayoutManager.LayoutState的next方法

View next(RecyclerView.Recycler recycler) {if (mScrapList != null) {return nextViewFromScrapList();}final View view = recycler.getViewForPosition(mCurrentPosition);mCurrentPosition += mItemDirection;return view;
}

mScrapList是在layoutForPredictiveAnimations中被赋值的,由于layoutForPredictiveAnimations的执行在fill之后,这里我们姑且把mScrapList当作null,尽管它不一定总是null,主要看怎么从recycler中取出对应的View的。

Recycler中的相关方法:

public View getViewForPosition(int position) {return getViewForPosition(position, false);
}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 there// 预布局的时候从mChangedScrap中查找ViewHolderif (mState.isPreLayout()) { // 预布局holder = getChangedScrapViewForPosition(position);fromScrapOrHiddenOrCache = holder != null;}// 1) Find by position from scrap/hidden list/cache// 1)先尝试通过position从mAttachedScrap、mChildHeppler的hidden列表和mCachedViews里查找if (holder == null) {holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);if (holder != null) {if (!validateViewHolderForOffsetPosition(holder)) {// 不能复用了,回收这个holderif (!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 {fromScrapOrHiddenOrCache = 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() + exceptionLabel());}final int type = mAdapter.getItemViewType(offsetPosition);// 2) 在通过id查找mAttachedScrap、mChildHeppler的hidden列表和mCachedViewsif (mAdapter.hasStableIds()) {holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),type, dryRun);if (holder != null) {// update positionholder.mPosition = offsetPosition;fromScrapOrHiddenOrCache = true;}}if (holder == null && mViewCacheExtension != null) {// We are NOT sending the offsetPosition because LayoutManager does not// know it.// 从mViewCacheExtension查找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"+ exceptionLabel());} else if (holder.shouldIgnore()) {throw new IllegalArgumentException("getViewForPositionAndType returned"+ " a view that is ignored. You must call stopIgnoring before"+ " returning this view." + exceptionLabel());}}}// 最后才从RecycledViewPool中查找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);}}}// 经过n此查找依然没有找到合适的ViewHolder,才通过Adapter创建新的ViewHolderif (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做了保存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);// 尝试绑定ViewHolderbound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);}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;}// 这里itemView通过持有LayoutParams间接持有了ViewHolder// 这样我们可以通过itemView找到对应的ViewHolder,也可以通过ViewHolder找到itemViewrvLayoutParams.mViewHolder = holder;rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;return 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;
}ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {final int scrapCount = mAttachedScrap.size();// Try first for an exact, non-invalid match from scrap.// 先查找mAttachedScrapfor (int i = 0; i < scrapCount; i++) {final ViewHolder holder = mAttachedScrap.get(i);if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position&& !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {// 标记FLAG_RETURNED_FROM_SCRAPholder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);return holder;}}if (!dryRun) { // 根绝上面的代码,dryRun为false// 这些被标记为hidden的View存储在ChildHelper中的mHiddenViews里// 这些View所谓的hidden并不是指View的visibility,而是它们不能被某些常规方法访问// 这里我们不深究View view = mChildHelper.findHiddenNonRemovedView(position);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 + exceptionLabel());}mChildHelper.detachViewFromParent(layoutIndex);scrapView(view);// 标记FLAG_RETURNED_FROM_SCRAP和FLAG_BOUNCED_FROM_HIDDEN_LISTvh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP| ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);return vh;}}// 从mCachedViews里查找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 getScrapOrCachedViewForIdif (!holder.isInvalid() && holder.getLayoutPosition() == position&& !holder.isAttachedToTransitionOverlay()) {if (!dryRun) {mCachedViews.remove(i);}if (DEBUG) {Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position+ ") found match in cache: " + holder);}return holder;}}return null;
}ViewHolder getScrapOrCachedViewForId(long id, int type, boolean dryRun) {// 依然是先查找mAttachedScrapfinal 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.// 这里可能会是以下两种情况之一:// item被移除了,但是当前是预布局// item被移除后又被添加到了其他的位置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更好,但是这会导致LayoutManager会对它进行布局,这又不太好// 回收掉,因为viewType变了mAttachedScrap.remove(i);removeDetachedView(holder.itemView, false);quickRecycleScrapView(holder.itemView);}}}// 从mCachedViews查找final int cacheSize = mCachedViews.size();for (int i = cacheSize - 1; i >= 0; i--) {final ViewHolder holder = mCachedViews.get(i);if (holder.getItemId() == id && !holder.isAttachedToTransitionOverlay()) {if (type == holder.getItemViewType()) {if (!dryRun) {mCachedViews.remove(i);}return holder;} else if (!dryRun) {recycleCachedViewAt(i);return null;}}}return null;
}private boolean tryBindViewHolderByDeadline(@NonNull ViewHolder holder, int offsetPosition,int position, long deadlineNs) {holder.mOwnerRecyclerView = RecyclerView.this;final int viewType = holder.getItemViewType();long startBindNs = getNanoTime();if (deadlineNs != FOREVER_NS&& !mRecyclerPool.willBindInTime(viewType, startBindNs, deadlineNs)) {// abort - we have a deadline we can't meetreturn false;}// 绑定ViewHoldermAdapter.bindViewHolder(holder, offsetPosition);long endBindNs = getNanoTime();mRecyclerPool.factorInBindTime(holder.getItemViewType(), endBindNs - startBindNs);attachAccessibilityDelegateOnBind(holder);if (mState.isPreLayout()) {holder.mPreLayoutPosition = position;}return true;
}

这段代码比较长,希望看到这里的同学耐心看完。总结一下要点:

  1. 预布局的时候,优先查找mChangedScrap,先position后id
  2. 先尝试通过position从mAttachedScrap、mChildHeppler的hidden列表和mCachedViews里查找ViewHolder
  3. 通过id从mAttachedScrap和mCachedViews里查找ViewHolder
  4. 从mViewCacheExtension查找ViewHolder
  5. 通过type从RecycledViewPool中查找ViewHolder
  6. 如果上述步骤都没有找到合适的ViewHolder,同过Adapter创建一个ViewHolder
  7. 根据需要绑定ViewHolder,只有needsUpdate()或者isInvalid()为true的时候才会执行ViewHolder的绑定,出现的情况有:Adapter的notifyItemChanged/notifyRangeChanged、notifyDatasetChanged相关方法以及新建ViewHolder或者从RecycledViewPool中取出ViewHolder

4、ViewHolder的回收

ViewHolder的回收(或者说缓存)有好几个地方,第一个就是上面分析的填充之前的LayoutManager#detachAndScrapAttachedViews,第二第三次分别在LinearLayoutManager#fill和RecyclerView#dispatchLayoutStep3中

先来看LinearLayoutManager#fill:

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,RecyclerView.State state, boolean stopOnFocusable) {// max offset we should set is mFastScroll + availablefinal int start = layoutState.mAvailable;if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {// TODO ugly bug fix. should not happenif (layoutState.mAvailable < 0) {layoutState.mScrollingOffset += layoutState.mAvailable;}recycleByLayoutState(recycler, layoutState);}int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;LayoutChunkResult layoutChunkResult = mLayoutChunkResult;while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {...layoutChunk(recycler, state, layoutState, layoutChunkResult);...if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {layoutState.mScrollingOffset += layoutChunkResult.mConsumed;if (layoutState.mAvailable < 0) {layoutState.mScrollingOffset += layoutState.mAvailable;}recycleByLayoutState(recycler, layoutState);}if (stopOnFocusable && layoutChunkResult.mFocusable) {break;}}if (DEBUG) {validateChildOrder();}return start - layoutState.mAvailable;
}

这里有两段相似的代码,一段位于开头,一段位于while循环里,根据注释,开头这一段是“丑陋的bug修复代码”,所以主要发挥作用的应该是后者。它们都是调用了recycleByLayoutState(recycler, layoutState)来回收超出限制范围的View

private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {if (!layoutState.mRecycle || layoutState.mInfinite) {return;}int scrollingOffset = layoutState.mScrollingOffset;int noRecycleSpace = layoutState.mNoRecycleSpace;if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {recycleViewsFromEnd(recycler, scrollingOffset, noRecycleSpace);} else {recycleViewsFromStart(recycler, scrollingOffset, noRecycleSpace);}
}

从布局方向的反方向回收,这里最终经过层层嵌套还是调用了Recycler#recycleViewHolderInternal,这个方法前面已经分析过了就不再赘述了。

再来看RecyclerView#dispatchLayoutStep3

private void dispatchLayoutStep3() {mState.assertLayoutStep(State.STEP_ANIMATIONS);startInterceptRequestLayout();onEnterLayoutOrScroll();mState.mLayoutStep = State.STEP_START;...mLayout.removeAndRecycleScrapInt(mRecycler);...
}

这里调用了LayoutManager的removeAndRecycleScrapInt方法

void removeAndRecycleScrapInt(Recycler recycler) {final int scrapCount = recycler.getScrapCount();// Loop backward, recycler might be changed by removeDetachedView()for (int i = scrapCount - 1; i >= 0; i--) {final View scrap = recycler.getScrapViewAt(i);final ViewHolder vh = getChildViewHolderInt(scrap);if (vh.shouldIgnore()) {continue;}// If the scrap view is animating, we need to cancel them first. If we cancel it// here, ItemAnimator callback may recycle it which will cause double recycling.// To avoid this, we mark it as not recycleable before calling the item animator.// Since removeDetachedView calls a user API, a common mistake (ending animations on// the view) may recycle it too, so we guard it before we call user APIs.vh.setIsRecyclable(false);if (vh.isTmpDetached()) {mRecyclerView.removeDetachedView(scrap, false);}if (mRecyclerView.mItemAnimator != null) {mRecyclerView.mItemAnimator.endAnimation(vh);}vh.setIsRecyclable(true);// 最终也是调用recycleViewHolderInternal(holder)recycler.quickRecycleScrapView(scrap);}recycler.clearScrap();if (scrapCount > 0) {mRecyclerView.invalidate();}
}

这里遍历了Recycler的mAttachedScrap,挨个回收,随后清空了mAttachedScrap和mChangedScrap,确保RecyclerView中没有多余的View。

5、总结

RecyclerView的缓存复用类Recycler有四级缓存机制:

  1. mAttachedScrap和mChangedScrap,用于临时存储可能还会停留在RecyclerView中的ViewHolder,在dispatchLayoutStep3中多余的ViewHolder会被缓存到mCachedViews或者RecycledViewPool
  2. mCachedViews,缓存上限是mViewCacheMax,默认值为2,缓存最后被回收的ViewHolder
  3. mViewCacheExtension,开发者自定义的缓存,Recycler本身并不会往里面put数据,只检索,其他逻辑由开发者实现
  4. RecycledViewPool,按照分类存储ViewHolder,进入这里的ViewHolder的position等都被重置了

另外,开头的234问题,到此也有了答案~

这篇关于RecyclerView源码分析(二):RecyclerView的缓存与复用机制的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1105638

相关文章

Springboot中分析SQL性能的两种方式详解

《Springboot中分析SQL性能的两种方式详解》文章介绍了SQL性能分析的两种方式:MyBatis-Plus性能分析插件和p6spy框架,MyBatis-Plus插件配置简单,适用于开发和测试环... 目录SQL性能分析的两种方式:功能介绍实现方式:实现步骤:SQL性能分析的两种方式:功能介绍记录

最长公共子序列问题的深度分析与Java实现方式

《最长公共子序列问题的深度分析与Java实现方式》本文详细介绍了最长公共子序列(LCS)问题,包括其概念、暴力解法、动态规划解法,并提供了Java代码实现,暴力解法虽然简单,但在大数据处理中效率较低,... 目录最长公共子序列问题概述问题理解与示例分析暴力解法思路与示例代码动态规划解法DP 表的构建与意义动

Spring排序机制之接口与注解的使用方法

《Spring排序机制之接口与注解的使用方法》本文介绍了Spring中多种排序机制,包括Ordered接口、PriorityOrdered接口、@Order注解和@Priority注解,提供了详细示例... 目录一、Spring 排序的需求场景二、Spring 中的排序机制1、Ordered 接口2、Pri

MySQL 缓存机制与架构解析(最新推荐)

《MySQL缓存机制与架构解析(最新推荐)》本文详细介绍了MySQL的缓存机制和整体架构,包括一级缓存(InnoDBBufferPool)和二级缓存(QueryCache),文章还探讨了SQL... 目录一、mysql缓存机制概述二、MySQL整体架构三、SQL查询执行全流程四、MySQL 8.0为何移除查

C#使用DeepSeek API实现自然语言处理,文本分类和情感分析

《C#使用DeepSeekAPI实现自然语言处理,文本分类和情感分析》在C#中使用DeepSeekAPI可以实现多种功能,例如自然语言处理、文本分类、情感分析等,本文主要为大家介绍了具体实现步骤,... 目录准备工作文本生成文本分类问答系统代码生成翻译功能文本摘要文本校对图像描述生成总结在C#中使用Deep

一文详解Java Condition的await和signal等待通知机制

《一文详解JavaCondition的await和signal等待通知机制》这篇文章主要为大家详细介绍了JavaCondition的await和signal等待通知机制的相关知识,文中的示例代码讲... 目录1. Condition的核心方法2. 使用场景与优势3. 使用流程与规范基本模板生产者-消费者示例

Go中sync.Once源码的深度讲解

《Go中sync.Once源码的深度讲解》sync.Once是Go语言标准库中的一个同步原语,用于确保某个操作只执行一次,本文将从源码出发为大家详细介绍一下sync.Once的具体使用,x希望对大家有... 目录概念简单示例源码解读总结概念sync.Once是Go语言标准库中的一个同步原语,用于确保某个操

一文带你理解Python中import机制与importlib的妙用

《一文带你理解Python中import机制与importlib的妙用》在Python编程的世界里,import语句是开发者最常用的工具之一,它就像一把钥匙,打开了通往各种功能和库的大门,下面就跟随小... 目录一、python import机制概述1.1 import语句的基本用法1.2 模块缓存机制1.

Redis主从/哨兵机制原理分析

《Redis主从/哨兵机制原理分析》本文介绍了Redis的主从复制和哨兵机制,主从复制实现了数据的热备份和负载均衡,而哨兵机制可以监控Redis集群,实现自动故障转移,哨兵机制通过监控、下线、选举和故... 目录一、主从复制1.1 什么是主从复制1.2 主从复制的作用1.3 主从复制原理1.3.1 全量复制

Redis缓存问题与缓存更新机制详解

《Redis缓存问题与缓存更新机制详解》本文主要介绍了缓存问题及其解决方案,包括缓存穿透、缓存击穿、缓存雪崩等问题的成因以及相应的预防和解决方法,同时,还详细探讨了缓存更新机制,包括不同情况下的缓存更... 目录一、缓存问题1.1 缓存穿透1.1.1 问题来源1.1.2 解决方案1.2 缓存击穿1.2.1