1.RecyclerView回收复用的谁? 2.RecyclerView有几级缓存? 3.如何实现自定义LayoutManager?
一级缓存(scrap):mChangedScrap 与 mAttachedScrap mChangedScrap 与 mAttachedScrap称为scrap,用来缓存屏幕内的ViewHolder。
这两个scrap就是第一级缓存,是优先级最高的缓存,RecyclerView在获取ViewHolder时,优先会到这两个缓存来找。其中mAttachedScrap存储的是当前还在屏幕中的ViewHolder,mChangedScrap存储的是数据被更新的ViewHolder,比如说调用了Adapter的notifyItemChanged方法。
RecyclerView之所以要将缓存分成这么多块肯定在功能上是有一定的区分的,它们分别对应不同的使用场景,scrap是用来保存被RecyclerView移除掉但最近又马上要使用的缓存,比如说RecyclerView中自带item的动画效果,本质上就是计算item的偏移量然后执行属性动画的过程,这中间可能就涉及到需要将动画之前的item保存下位置信息,动画后的item再保存下位置信息,然后利用这些位置数据生成相应的属性动画。如何保存这些viewholer呢,就需要使用到scrap了,因为这些viewholer数据上是没有改变的,只是位置改变而已,所以放置到scrap最为合适。稍微仔细看的话就能发现scrap缓存有两个成员mChangedScrap和mAttachedScrap,它们保存的对象有些不一样,一般调用adapter的notifyItemRangeChanged被移除的viewholder会保存到mChangedScrap,其余的notify系列方法(不包括notifyDataSetChanged)移除的viewholder会被保存到mAttachedScrap中。
二级缓存(cache):mCachedViews 用来缓存移除屏幕之外的ViewHolder ,默认大小为2。
也是RecyclerView中非常重要的一个缓存,就linearlayoutmanager来说cached缓存默认大小为2,它的容量非常小,所起到的作用就是RecyclerView滑动时刚被移出屏幕的viewholer的收容所,因为RecyclerView会认为刚被移出屏幕的viewholder可能接下来马上就会使用到,所以不会立即设置为无效viewholer,会将它们保存到cached中,但又不能将所有移除屏幕的viewholder都视为有效viewholer,所以它的默认容量只有2个。
三级缓存(cacheExtension):mViewCacheExtension 开发给用户的自定义扩展缓存,需要用户自己管理View的创建和缓存 ,通常用不到。 如果使用,需要调用Recycler的setViewCacheExtension(ViewCacheExtension extension)方法进行设定。
四级缓存(pool):RecycledViewPool ViewHolder 缓存池。 根据ViewType来缓存ViewHolder,每个ViewType的数组大小默认为5。
保存的对象就是那些无效的ViewHolder ,虽说无效的ViewHolder 上的数据是无效的,但是它的rootview还是可以拿来使用的,RecycledViewPool一般会和mCachedViews配合使用,mCachedViews存不下的会被保存到RecycledViewPool中,毕竟mCachedViews默认容量大小只有2,但是RecycledViewPool容量也是有限的,当保存满之后再有ViewHolder添加的话会直接丢弃。
使用多级缓存的目的:为了提高性能。
对于传统的AdapterView,需要在实现的Adapter类中手动加ViewHolder,RecyclerView直接将ViewHolder内置,并在原来基础上功能上更强大。ViewHolder描述RecylerView中某个位置的itemView和元数据信息,属于Adapter的一部分。其实现类通常用于保存findViewById的结果。 主要元素组成有:
public static abstract class ViewHolder { public final View itemView;//itemView WeakReference<RecyclerView> mNestedRecyclerView; int mPosition = NO_POSITION; //位置 int mOldPosition = NO_POSITION;//上一次的位置 long mItemId = NO_ID; int mItemViewType = INVALID_TYPE; int mPreLayoutPosition = NO_POSITION; // The item that this holder is shadowing during an item change event/animation ViewHolder mShadowedHolder = null; // The item that is shadowing this holder during an item change event/animation ViewHolder mShadowingHolder = null; /** * This ViewHolder has been bound to a position; mPosition, mItemId and mItemViewType * are all valid. */ static final int FLAG_BOUND = 1 << 0; /** * The data this ViewHolder's view reflects is stale and needs to be rebound * by the adapter. mPosition and mItemId are consistent. */ static final int FLAG_UPDATE = 1 << 1; /** * This ViewHolder's data is invalid. The identity implied by mPosition and mItemId * are not to be trusted and may no longer match the item view type. * This ViewHolder must be fully rebound to different data. */ static final int FLAG_INVALID = 1 << 2; /** * This ViewHolder points at data that represents an item previously removed from the * data set. Its view may still be used for things like outgoing animations. */ static final int FLAG_REMOVED = 1 << 3; /** * This ViewHolder should not be recycled. This flag is set via setIsRecyclable() * and is intended to keep views around during animations. */ static final int FLAG_NOT_RECYCLABLE = 1 << 4; /** * This ViewHolder is returned from scrap which means we are expecting an addView call * for this itemView. When returned from scrap, ViewHolder stays in the scrap list until * the end of the layout pass and then recycled by RecyclerView if it is not added back to * the RecyclerView. */ static final int FLAG_RETURNED_FROM_SCRAP = 1 << 5; /** * This ViewHolder is fully managed by the LayoutManager. We do not scrap, recycle or remove * it unless LayoutManager is replaced. * It is still fully visible to the LayoutManager. */ static final int FLAG_IGNORE = 1 << 7; /** * When the View is detached form the parent, we set this flag so that we can take correct * action when we need to remove it or add it back. */ static final int FLAG_TMP_DETACHED = 1 << 8; /** * Set when we can no longer determine the adapter position of this ViewHolder until it is * rebound to a new position. It is different than FLAG_INVALID because FLAG_INVALID is * set even when the type does not match. Also, FLAG_ADAPTER_POSITION_UNKNOWN is set as soon * as adapter notification arrives vs FLAG_INVALID is set lazily before layout is * re-calculated. */ static final int FLAG_ADAPTER_POSITION_UNKNOWN = 1 << 9; /** * Set when a addChangePayload(null) is called */ static final int FLAG_ADAPTER_FULLUPDATE = 1 << 10; /** * Used by ItemAnimator when a ViewHolder's position changes */ static final int FLAG_MOVED = 1 << 11; /** * Used by ItemAnimator when a ViewHolder appears in pre-layout */ static final int FLAG_APPEARED_IN_PRE_LAYOUT = 1 << 12; static final int PENDING_ACCESSIBILITY_STATE_NOT_SET = -1; /** * Used when a ViewHolder starts the layout pass as a hidden ViewHolder but is re-used from * hidden list (as if it was scrap) without being recycled in between. * * When a ViewHolder is hidden, there are 2 paths it can be re-used: * a) Animation ends, view is recycled and used from the recycle pool. * b) LayoutManager asks for the View for that position while the ViewHolder is hidden. * * This flag is used to represent "case b" where the ViewHolder is reused without being * recycled (thus "bounced" from the hidden list). This state requires special handling * because the ViewHolder must be added to pre layout maps for animations as if it was * already there. */ static final int FLAG_BOUNCED_FROM_HIDDEN_LIST = 1 << 13; /** * Flags that RecyclerView assigned {@link RecyclerViewAccessibilityDelegate * #getItemDelegate()} in onBindView when app does not provide a delegate. */ static final int FLAG_SET_A11Y_ITEM_DELEGATE = 1 << 14; int mFlags; ... }关于ViewHolder,重点介绍的是mFlags。 FLAG_BOUND——ViewHolder已经绑定到某个位置,mPosition、mItemId、mItemViewType都有效 FLAG_UPDATE——ViewHolder绑定的View对应的数据过时,需要重新绑定,mPosition、mItemId还是一致的。 FLAG_INVALID——ViewHolder绑定的View对应的数据无效,需要完全重新绑定不同的数据 ,mPosition、mItemId已经和ViewHolder不对应。 FLAG_REMOVED——ViewHolder对应的数据已经从数据集移除 FLAG_NOT_RECYCLABLE——ViewHolder不能复用 FLAG_RETURNED_FROM_SCRAP——这个状态的ViewHolder会加到scrap list被复用。 FLAG_CHANGED——ViewHolder内容发生变化,通常用于表明有ItemAnimator动画 FLAG_IGNORE——ViewHolder完全由LayoutManager管理,不能复用 FLAG_TMP_DETACHED——ViewHolder从父RecyclerView临时分离的标志,便于后续移除或添加回来 FLAG_ADAPTER_POSITION_UNKNOWN——ViewHolder不知道对应的Adapter的位置,直到绑定到一个新位置 FLAG_ADAPTER_FULLUPDATE——方法addChangePayload(null)调用时设置
回收什么?复用什么? 回收复用的是ViewHolder
回收到哪里去?从哪里获得复用? 四级缓存,分别对应四种集合:
mAttachedScrap 和 mChangedScrapmCachedViewsmViewCacheExtension 这个的创建和缓存完全由开发者自己控制,系统未往这里添加数据RecycledViewPool什么时候回收?什么时候复用? 在页面进行布局( RecyclerView#onLayout() )和滑动( RecyclerView#onTouchEvent() )时会进行回收复用。
在页面进行布局( RecyclerView#onLayout() )和滑动( RecyclerView#onTouchEvent() )时会进行回收复用。
注意布局发生的场景有很多,比如: 1.第一次显示RecyclerView时 2.页面横竖屏切换改变RecyclerView布局时 3.RecyclerView重新排序屏幕中item时(可以做这种功能) 等等
1.页面进行布局( RecyclerView#onLayout() )的复用(即读取缓存)时序图:
2.滑动( RecyclerView#onTouchEvent() )时的复用(即读取缓存)的调用过程: onTouchEvent() --> scrollByInternal() --> scrollStep() --> mLayout.scrollVerticallyBy() –> scrollBy() --> fill() --> layoutChunk() --> View view = layoutState.next(recycler); addView(view);
源码入口:onTouchEvent()的ACTION_MOVE事件
//RecyclerView.java @Override public boolean onTouchEvent(MotionEvent e) { ... case MotionEvent.ACTION_MOVE: { ... if (scrollByInternal( canScrollHorizontally ? dx : 0, canScrollVertically ? dy : 0, vtev)) { getParent().requestDisallowInterceptTouchEvent(true); } ... } break; ... }fill()方法:获取itemview填充RecyclerView
//LinearLayoutManager.java /** * The magic functions :). Fills the given layout, defined by the layoutState. This is fairly * independent from the rest of the {@link LinearLayoutManager} * and with little change, can be made publicly available as a helper class. * * @param recycler Current recycler that is attached to RecyclerView * @param layoutState Configuration on how we should fill out the available space. * @param state Context passed by the RecyclerView to control scroll steps. * @param stopOnFocusable If true, filling stops in the first focusable new child * @return Number of pixels that it added. Useful for scroll functions. */ int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) { // max offset we should set is mFastScroll + available final int start = layoutState.mAvailable; if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) { // TODO ugly bug fix. should not happen if (layoutState.mAvailable < 0) { layoutState.mScrollingOffset += layoutState.mAvailable; } //滑动时回收ViewHolder recycleByLayoutState(recycler, layoutState); } int remainingSpace = layoutState.mAvailable + layoutState.mExtra; LayoutChunkResult layoutChunkResult = mLayoutChunkResult; //循环判断RecyclerView是否还有空间,如果有,执行layoutChunk方法填充RecyclerView,直至填充满。 while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) { layoutChunkResult.resetInternal(); if (RecyclerView.VERBOSE_TRACING) { TraceCompat.beginSection("LLM LayoutChunk"); } //复用 layoutChunk(recycler, state, layoutState, layoutChunkResult); if (RecyclerView.VERBOSE_TRACING) { TraceCompat.endSection(); } if (layoutChunkResult.mFinished) { break; } layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection; /** * Consume the available space if: * * layoutChunk did not request to be ignored * * OR we are laying out scrap children * * OR we are not doing pre-layout */ if (!layoutChunkResult.mIgnoreConsumed || mLayoutState.mScrapList != null || !state.isPreLayout()) { layoutState.mAvailable -= layoutChunkResult.mConsumed; // we keep a separate remaining space because mAvailable is important for recycling remainingSpace -= layoutChunkResult.mConsumed; } 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; }从缓存中获取View就是layoutState.next(recycler)方法:
//LinearLayoutManager.java /** * Gets the view for the next element that we should layout. * Also updates current item index to the next item, based on {@link #mItemDirection} * * @return The next element that we should layout. */ View next(RecyclerView.Recycler recycler) { if (mScrapList != null) { return nextViewFromScrapList(); } final View view = recycler.getViewForPosition(mCurrentPosition); mCurrentPosition += mItemDirection; return view; }layoutState.next() --> getViewForPosition() --> tryGetViewHolderForPositionByDeadline()
tryGetViewHolderForPositionByDeadline()方法主要功能就是从缓存中获取ViewHolder,分析下该方法:
//RecyclerView.java /** * Attempts to get the ViewHolder for the given position, either from the Recycler scrap, * cache, the RecycledViewPool, or creating it directly. * <p> * If a deadlineNs other than {@link #FOREVER_NS} is passed, this method early return * rather than constructing or binding a ViewHolder if it doesn't think it has time. * If a ViewHolder must be constructed and not enough time remains, null is returned. If a * ViewHolder is aquired and must be bound but not enough time remains, an unbound holder is * returned. Use {@link ViewHolder#isBound()} on the returned object to check for this. * * @param position Position of ViewHolder to be returned. * @param dryRun True if the ViewHolder should not be removed from scrap/cache/ * @param deadlineNs Time, relative to getNanoTime(), by which bind/create work should * complete. If FOREVER_NS is passed, this method will not fail to * create/bind the holder if needed. * * @return ViewHolder for requested position */ @Nullable 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 if (mState.isPreLayout()) { /** 判断mInPreLayout变量(默认为false),当有动画时此变量才为true,即只有有动画时,mChangedScrap才起作用。 */ holder = getChangedScrapViewForPosition(position); fromScrapOrHiddenOrCache = holder != null; } // 1) Find by position from scrap/hidden list/cache if (holder == null) { holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun); if (holder != null) { if (!validateViewHolderForOffsetPosition(holder)) { // recycle holder (and unscrap if relevant) since it can't be used 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 { 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) Find from scrap/cache via stable ids, if exists if (mAdapter.hasStableIds()) { holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun); if (holder != null) { // update position holder.mPosition = offsetPosition; fromScrapOrHiddenOrCache = 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" + exceptionLabel()); } else if (holder.shouldIgnore()) { throw new IllegalArgumentException("getViewForPositionAndType returned" + " a view that is ignored. You must call stopIgnoring before" + " returning this view." + exceptionLabel()); } } } if (holder == null) { // fallback to pool if (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); } } } 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; } 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; 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; }分几种情况去各种缓存集合中获取缓存的ViewHolder:
getChangedScrapViewForPosition() mChangeScrap, 与动画相关
getScrapOrHiddenOrCachedHolderForPosition() mAttachedScrap 、mCachedViews
getScrapOrCachedViewForId() mAttachedScrap 、mCachedViews
mViewCacheExtension.getViewForPositionAndType() 自定义缓存
getRecycledViewPool().getRecycledView() 从缓冲池里面获取
当各级缓存都没有读取到的时候调用 mAdapter.createViewHolder() --> onCreateViewHolder() 创建ViewHolder
获取ViewHolder后根据ViewHolder的mFlags值判断是否需要调用tryBindViewHolderByDeadline() 方法(当ViewHolder还没有绑定过或者需要更新数据或者数据已经无效时会调用): tryBindViewHolderByDeadline() --> mAdapter.bindViewHolder() --> onBindViewHolder()
所谓回收,就是看RecyclerView是怎么往四级缓存中添加ViewHolder的。
在页面进行布局( RecyclerView#onLayout() )和滑动( RecyclerView#onTouchEvent() )时会进行回收复用。
1.在页面进行滑动( RecyclerView#onTouchEvent() )时写入缓存的调用过程: onTouchEvent() --> scrollByInternal() --> scrollStep() --> mLayout.scrollVerticallyBy() –> scrollBy() --> fill -->recycleByLayoutState --> recycleViewsFromStart --> recycleChildren –> removeAndRecycleViewAt --> recycler.recycleView –> recycler.recycleViewHolderInternal(viewHolder);
滑动时调用fill方法时,fill方法会判断是否调用recycleByLayoutState 方法进行回收ViewHolder
可以看到滑动时回收View最终是直接调用recycleViewHolderInternal()将ViewHolder直接缓存到mCachedViews和RecyclerViewPool 中,而不会像布局时那样调用scrapOrRecycleView()方法判断是否将ViewHolder放入scrap中。
2.在页面进行布局( RecyclerView#onLayout() )时写入缓存的时序图:
LinearLayoutManager.onLayoutChildren() --> detachAndScrapAttachedViews() --> scrapOrRecycleView()
//LinearLayoutManager.java @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { // layout algorithm: // 1) by checking children and other variables, find an anchor coordinate and an anchor // item position. // 2) fill towards start, stacking from bottom // 3) fill towards end, stacking from top // 4) scroll to fulfill requirements like stack from bottom. // create layout state ... //在布局之前,将所有子View先detach掉,并放入缓存(包括四级缓存)中 detachAndScrapAttachedViews(recycler); ... //然后调用fill()方法循环不断从缓存中取出View加入到RecyclerView中,直到占满RecyclerView fill(recycler, mLayoutState, state, false); ... }为什么LayoutManager需要先执行detach,然后再重新attach这些view呢? 是为了隔离LayoutManager和RecyclerView.Recycler之间的关注点/职责。LayoutManager不需要知道哪些子view需要保留或者被回收到RecyclerViewPool或者其他地方,这是Recycler的职责。
//RecyclerView.java /** * Temporarily detach and scrap all currently attached child views. Views will be scrapped * into the given Recycler. The Recycler may prefer to reuse scrap views before * other views that were previously recycled. * * @param recycler Recycler to scrap views into */ 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); if (viewHolder.shouldIgnore()) { if (DEBUG) { Log.d(TAG, "ignoring view " + viewHolder); } return; } if (viewHolder.isInvalid() && !viewHolder.isRemoved() && !mRecyclerView.mAdapter.hasStableIds()) { removeViewAt(index); recycler.recycleViewHolderInternal(viewHolder);//缓存到mCachedViews和RecyclerViewPool } else { detachViewAt(index); recycler.scrapView(view);//将view缓存到scrap(即mAttachedScrap和mChangedScrap) mRecyclerView.mViewInfoStore.onViewDetached(viewHolder); } }RecyclerView回收View有两种方式:Detach和Remove。Detach的View放在Scrap缓存中,Remove掉的View放在mCachedViews和RecyclerViewPool缓存中;那我们应该如何去选择呢?
在什么样的场景中使用Detach呢?主要是在我们的代码执行结束之前,我们需要反复去将View移除并且马上又要添加进去时,选择Detach方式,比如:当我们对View进行重新排序的时候,可以选择Detach,因为屏幕上显示的就是这些position对应的View,我们并不需要重新去绑定数据,这明显可以提高效率。使用Detach的方式可以通过函数detachAndScrapView()实现。
在什么样的场景中使用Remove呢?使用Remove的方式,是当View不在屏幕中有任何显示的时候,你需要将它Remove掉,以备后面循环利用,比如滑动RecyclerView时,滑出屏幕的itemview就是使用Remove的方式进行回收。使用Remove的方式可以通过函数removeAndRecycleView()实现。
缓存到mCachedViews和RecyclerViewPool 。
/** * 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) { if (holder.isScrap() || holder.itemView.getParent() != null) { throw new IllegalArgumentException( "Scrapped or attached views may not be recycled. isScrap:" + holder.isScrap() + " isAttached:" + (holder.itemView.getParent() != null) + exceptionLabel()); } if (holder.isTmpDetached()) { throw new IllegalArgumentException("Tmp detached view should be removed " + "from RecyclerView before it can be recycled: " + holder + exceptionLabel()); } if (holder.shouldIgnore()) { throw new IllegalArgumentException("Trying to recycle an ignored view holder. You" + " should first call stopIgnoringView(view) before calling recycle." + exceptionLabel()); } //noinspection unchecked final boolean transientStatePreventsRecycling = holder .doesTransientStatePreventRecycling(); 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()) { //如果ViewHodler没有改变 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(); /** 如果mCachedViews.size大于mViewCacheMax(默认是DEFAULT_CACHE_SIZE = 2;), 则调用recycleCachedViewAt(0)将mCachedViews中最旧的ViewHolder加入RecycledViewPool缓存池中, 并在mCachedViews中将最旧的ViewHolder删除。所以缓存池里面的数据都是从mCachedViews里面出来的。 */ 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; } //如果没有缓存到mCachedViews中,则直接加入到RecycledViewPool缓存池中 if (!cached) { addViewHolderToRecycledViewPool(holder, true); recycled = true; } } else { // NOTE: A view can fail to be recycled when it is scrolled off while an animation // runs. In this case, the item is eventually recycled by // ItemAnimatorRestoreListener#onAnimationFinished. // TODO: consider cancelling an animation when an item is removed scrollBy, // to return it to the pool faster 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. mViewInfoStore.removeViewHolder(holder); if (!cached && !recycled && transientStatePreventsRecycling) { holder.mOwnerRecyclerView = null; } }mCachedViews和RecyclerViewPool: 添加ViewHolder时,如果mCachedViews中缓存已满,则先将mCachedViews中的第0个元素移除并放入RecycledViewPool缓存池中,然后将待添加的ViewHolder加入到mCachedViews中。
RecycledViewPool 的putRecycledView()方法先清空ViewHolder的数据,然后才将ViewHolder加入缓存池中,所以RecycledViewPool缓存池中缓存的是ViewHolder类型,ViewHolder里面没有数据。这与mCachedViews是不同的,mCachedViews缓存的是带有数据的ViewHolder。
缓存到scrap(即mAttachedScrap和mChangedScrap)。
//RecyclerView.java /** * Mark an attached view as scrap. * * <p>"Scrap" views are still attached to their parent RecyclerView but are eligible * for rebinding and reuse. Requests for a view for a given position may return a * reused or rebound scrap view instance.</p> * * @param view View to scrap */ 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); mAttachedScrap.add(holder); } else { if (mChangedScrap == null) { mChangedScrap = new ArrayList<ViewHolder>(); } holder.setScrapContainer(this, true); mChangedScrap.add(holder); } }这个方法根本的目的就是,判断ViewHolder的flag状态,从而来决定是放入mAttachedScrap还是mChangedScrap。从上面的代码,我们得出:
mAttachedScrap里面放的是两种状态的ViewHolder: 1). 被同时标记为remove和invalid的ViewHolder; 2). 完全没有改变的ViewHolder,即不需要更新的ViewHolder。 3). 这里还有第三个判断,这个跟RecyclerView的ItemAnimator有关,如果ItemAnimator为空或者ItemAnimator的canReuseUpdatedViewHolder方法为true,也会放入到mAttachedScrap。
那么mChangedScrap里面放什么类型flag的ViewHolder呢?当然是ViewHolder的isUpdated方法返回为true时,会放入到mChangedScrap里面去。所以,调用Adapter的notifyItemChanged方法时,并且RecyclerView的ItemAnimator不为空,会放入到mChangedScrap里面。即mChangedScrap用来保存RecyclerView做动画时,被detach的ViewHolder。
notifyDataSetChanged–>mObservable.notifyChanged –> (RecyclerViewDataObserver)mObservers.get(i).onChanged --> requestLayout
RecyclerView在填充item的过程中,每填充一个item,RecyclerView的bottom都会加上这个item的高度,直到RecyclerView的bottom大于屏幕的高度,填充完毕。
https://developer.android.google.cn/reference/androidx/recyclerview/widget/RecyclerView?hl=en
RecyclerView一些你可能需要知道的优化技术 RecyclerView 源码分析(三) - RecyclerView的缓存机制 RecyclerView缓存机制(scrap view) 打造属于你的LayoutManager
基于场景解析RecyclerView的回收复用机制原理 RecyclerView源码分析 Android源码分析之RecyclerView源码分析(二)——缓存机制 Android-RecyclerView布局显示和回收复用流程
阿里3轮面试都问了RecyclerView 阿里3轮面试都问了RecyclerView 地狱难度!字节跳动Android高级岗:说说RecyclerView的回收复用机制