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

相关文章

使用Spring Cache本地缓存示例代码

《使用SpringCache本地缓存示例代码》缓存是提高应用程序性能的重要手段,通过将频繁访问的数据存储在内存中,可以减少数据库访问次数,从而加速数据读取,:本文主要介绍使用SpringCac... 目录一、Spring Cache简介核心特点:二、基础配置1. 添加依赖2. 启用缓存3. 缓存配置方案方案

基于Redis自动过期的流处理暂停机制

《基于Redis自动过期的流处理暂停机制》基于Redis自动过期的流处理暂停机制是一种高效、可靠且易于实现的解决方案,防止延时过大的数据影响实时处理自动恢复处理,以避免积压的数据影响实时性,下面就来详... 目录核心思路代码实现1. 初始化Redis连接和键前缀2. 接收数据时检查暂停状态3. 检测到延时过

Redis中哨兵机制和集群的区别及说明

《Redis中哨兵机制和集群的区别及说明》Redis哨兵通过主从复制实现高可用,适用于中小规模数据;集群采用分布式分片,支持动态扩展,适合大规模数据,哨兵管理简单但扩展性弱,集群性能更强但架构复杂,根... 目录一、架构设计与节点角色1. 哨兵机制(Sentinel)2. 集群(Cluster)二、数据分片

Java实现本地缓存的四种方法实现与对比

《Java实现本地缓存的四种方法实现与对比》本地缓存的优点就是速度非常快,没有网络消耗,本地缓存比如caffine,guavacache这些都是比较常用的,下面我们来看看这四种缓存的具体实现吧... 目录1、HashMap2、Guava Cache3、Caffeine4、Encache本地缓存比如 caff

Android 缓存日志Logcat导出与分析最佳实践

《Android缓存日志Logcat导出与分析最佳实践》本文全面介绍AndroidLogcat缓存日志的导出与分析方法,涵盖按进程、缓冲区类型及日志级别过滤,自动化工具使用,常见问题解决方案和最佳实... 目录android 缓存日志(Logcat)导出与分析全攻略为什么要导出缓存日志?按需过滤导出1. 按

深入理解go中interface机制

《深入理解go中interface机制》本文主要介绍了深入理解go中interface机制,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学... 目录前言interface使用类型判断总结前言go的interface是一组method的集合,不

Linux中的HTTPS协议原理分析

《Linux中的HTTPS协议原理分析》文章解释了HTTPS的必要性:HTTP明文传输易被篡改和劫持,HTTPS通过非对称加密协商对称密钥、CA证书认证和混合加密机制,有效防范中间人攻击,保障通信安全... 目录一、什么是加密和解密?二、为什么需要加密?三、常见的加密方式3.1 对称加密3.2非对称加密四、

MySQL中读写分离方案对比分析与选型建议

《MySQL中读写分离方案对比分析与选型建议》MySQL读写分离是提升数据库可用性和性能的常见手段,本文将围绕现实生产环境中常见的几种读写分离模式进行系统对比,希望对大家有所帮助... 目录一、问题背景介绍二、多种解决方案对比2.1 原生mysql主从复制2.2 Proxy层中间件:ProxySQL2.3

C# async await 异步编程实现机制详解

《C#asyncawait异步编程实现机制详解》async/await是C#5.0引入的语法糖,它基于**状态机(StateMachine)**模式实现,将异步方法转换为编译器生成的状态机类,本... 目录一、async/await 异步编程实现机制1.1 核心概念1.2 编译器转换过程1.3 关键组件解析

python使用Akshare与Streamlit实现股票估值分析教程(图文代码)

《python使用Akshare与Streamlit实现股票估值分析教程(图文代码)》入职测试中的一道题,要求:从Akshare下载某一个股票近十年的财务报表包括,资产负债表,利润表,现金流量表,保存... 目录一、前言二、核心知识点梳理1、Akshare数据获取2、Pandas数据处理3、Matplotl