本篇文章从源码的角度分析RecyclerView的复用
和回收
的原理机制
复用机制
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) {
}
RecyclerView的复用机制源码就在tryGetViewHolderForPositionByDeadline
方法中通过复用或创建拿到ViewHolder
,然后通过 ViewHolder.itemView
来获取需要的View
。
RecyclerView缓存ViewHolder的Recycler
的结构体:
public final class Recycler {
final ArrayList mAttachedScrap = new ArrayList<>();
ArrayList mChangedScrap = null;
// 重点分析缓存
final ArrayList mCachedViews = new ArrayList();
private final List
mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);
private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
int mViewCacheMax = DEFAULT_CACHE_SIZE;
// 重点分析缓存
RecycledViewPool mRecyclerPool;
private ViewCacheExtension mViewCacheExtension;
static final int DEFAULT_CACHE_SIZE = 2;
}
mAttachedScrap:保存还在屏幕在上的Item的ViewHolder。比如在onLayout()时会先把ItemView去掉,在重新add添加进去,这个List可能就保存这种临时的ViewHolder。
mChangedScrap: 保存那些被标记跟新、有动画的ViewHolder
mCachedViews: 保存刚刚滑出屏幕上的ViewHolder,大小默认为2个(可以通过setItemViewCacheSize来设置大小)。这里的ViewHolder通过position或id可以找到,且可以直接拿来复用,不需要执行onBindViewHolder。
RecycledViewPool:根据ViewType保存被滑出屏幕的ViewHolder,每种ViewType最多保存五个ViewHodler。复用这里的ViewHolder数据已被清除,需要执行onBindViewHolder()方法。
RecyclerView大致绘制流程:
- RecyclerView的RequestLayout开始绘制
- layout开始,调用LayoutManager.fill()方法将RecyclerView填满
- LayoutManager.fill会循环调用layoutChunk()方法生成View并addView到RV
- layoutChunk调用tryGetViewHolderForPositionByDeadline()方法复用或创建一个ViewHolder。
tryGetViewHolderForPositionByDeadline源码
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());
}
检查position的下标,越界后就抛异常
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
//...省略position越界检查代码
boolean fromScrapOrHiddenOrCache = false;
ViewHolder holder = null;
// 0) If there is a changed scrap, try to find from there
//.isPreLayout()要设置了动画才为true,所以一下代码默认不执行
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
如果isPreLayout为true就到ChangedScrap里面找,看看isPreLayout在什么地方赋值:
protected void onMeasure(int widthSpec, int heightSpec) {
//...省略无关代码
if (mLayout.mAutoMeasure) {
//...
} else {
// custom onMeasure
if (mAdapterUpdateDuringMeasure) {
if (mState.mRunPredictiveAnimations) {
mState.mInPreLayout = true;
} else {
// consume remaining updates to provide a consistent state with the layout pass.
mState.mInPreLayout = false;
}
}
}
//...
mState.mInPreLayout = false; // clear
}
private void dispatchLayoutStep1() {
mState.mInPreLayout = mState.mRunPredictiveAnimations
}
private void dispatchLayoutStep2() {
mState.mInPreLayout = false;
mLayout.onLayoutChildren(mRecycler, mState);
}
可以看到在执行mLayout.onLayoutChildren之前会把mInPreLayout = false。所以暂时不用管它。继续扫描代码。
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
//...省略其他代码
// 1) Find by position from scrap/hidden list/cache
if (holder == null) {
// 分别从mAttachedScrap、HiddenViews(dryRun为false)、mCachedViews获取复用的Viewholder
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
//如果拿到ViewHolder后,
if (holder != null) {
// 验证ViewHolder能否直接拿来复用
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.
// 如果不能复用,就放到CacheView后ViewPool里
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;
}
}
}
}
我们跟进去看看获取复用的代码:
ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
final int scrapCount = mAttachedScrap.size();
// 遍历AttachedScrap,寻找符合位置与状态的ViewHolder
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())) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
// 如果dryRun为false,尝试从HiddenViews复用ViewHolder
if (!dryRun) {
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);
vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP
| ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
return vh;
}
}
// 遍历CachedViews,尝试从位置和状态符合的ViewHolder
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 getScrapOrCachedViewForId
if (!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;
}
从上面的代码可以看到,先从mAttachedScrap
遍历寻找position一致的,状态有效、没有更新的ViewHolder。如果dryRun==false
,则从HiddenViews
中寻找position一致的,状态有效、没有更新的ViewHolder。最后,从mCachedViews
中寻找position一致的,状态有效、没有更新的ViewHolder。
如果从上面三个List寻找到ViewHolder,则再次判断ViewHolder是否符合条件,可以直接复用
。不符合条件则置holder为null。
如果上面的List中通过position找不到ViewHolder,并且你设置了 adapter.setHasStableIds(true)
那么就通过id来找。
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
//...省略看过代码
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()) {
//根据id和type去AttachView和CacheView中寻找ViewHolder
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
if (holder != null) {
// update position
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
}
}
}
getScrapOrCachedViewForId和getScrapOrHiddenOrCachedHolderForPosition的寻找差不多,这里就不贴出代码分析。
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
//...省略看过代码
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());
}
}
}
}
mViewCacheExtension是给我们自定义复用ViewHolder的,一般没用,继续往下。
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
//...省略看过代码
final int type = mAdapter.getItemViewType(offsetPosition);
if (holder == null) { // fallback to pool
if (DEBUG) {
Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
+ position + ") fetching from shared pool");
}
// 从ViewPool寻找复用
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
// 如果从ViewPool中找到holder,则置空里面的数据
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
}
从ViewPool中根据ViewType中获取一个ViewHolder,并且在得到ViewHolder后,将ViewHolder的相关数据重置,这也就是为什么要重新执行onBindViewHolder()。看看如何获取ViewHolder.
public ViewHolder getRecycledView(int viewType) {
final ScrapData scrapData = mScrap.get(viewType);
if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
final ArrayList scrapHeap = scrapData.mScrapHeap;
// 把List的最后一个返回并remove
for (int i = scrapHeap.size() - 1; i >= 0; i--) {
if (!scrapHeap.get(i).isAttachedToTransitionOverlay()) {
return scrapHeap.remove(i);
}
}
}
return null;
}
ViewPool的存储结构为
,先根据type拿到对应的ScrapData类,ScrapData类中存储一个ViewHolder集合变量,然后从这个集合中获取最后下标的数据返回并移除。
如果在ViewPool中都没有找到ViewHolder,那就继续往下看。
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
//...省略看过代码
if (holder == null) {
//...省略无关代码
holder = mAdapter.createViewHolder(RecyclerView.this, type);
//...省略无关代码
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
}
}
这里就开始调用mAdapter.createViewHolder
来创建ViewHolder了。这里就是我们的adapter的流程了,创建一个VH,而不是复用VH。
到这里,方法tryGetViewHolderForPositionByDeadline()
的复用的ViewHolder机制就分析完成了。
复用机制方法总结:
- RecyclerView可分为四级缓存:
-
mChangeScrap和mAttachScrap
为一级缓存:主要复用还在屏幕上的ViewHolder。mChangeScrap通过position进行判断,mAttachScrap可以通过position或id进行判断ViewHolder。 -
mCacheView
为二级缓存:默认大小为2,主要存储刚刚滑出屏幕的ViewHolder。 第一级和第二级缓存都是通过postion或id进行判断ViewHolder,而且不需要执行onCreateViewHolder和onBindViewHolder两个方法 -
mViewCacheExtension
为三级缓存:可以自己定义如何复用,但一般不使用。 -
mRecylerPool
为四级缓存:这里是最后找复用的地方,每个ViewType可以有装5个ViewHolder,根据ViewType来查找是否有复用的ViewHolder。如果有可复用的ViewHolder,则将ViewHolder的数据重置,并执行onBindViewHolder方法。
- 源码角度分析复用:
- 如果
mInPreLayout == true
时,才根据position
查询mChangeScrap是否有可复用的ViewHolder。mInPreLayout
一般在有动画时才为true
。否则不复用mChangeView。 - 根据
position
分别查询mAttachScrap、hiddenViews、mCacheView是否有可复用的ViewHolder。 - 如果设置了
mHasStableIds==true
,则根据id
查询mAttachScrap、mCacheView是否有可复用的ViewHolder。 - 根据
type
在mRecycerPool查询可复用的ViewHolder,并且情况数据,重新bind。 - 复用流程都没有找到ViewHolder,则执行正常的创建ViewHolder流程。
回收机制
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()) {
// 移除相关位置的View
removeViewAt(index);
// 拿去CacheView、RecyclerPool中回收
recycler.recycleViewHolderInternal(viewHolder);
} else {
//增加相关位置的view
detachViewAt(index);
//将ViewHolder回收到mAttachScrap或mChangeScrap中
recycler.scrapView(view);
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
}
}
在scrapOrRecycleView
方法中,会根据ViewHolder的状态
进行不同的回收操作。如果ViewHolder的状态是失效的、没有被移除、没有设置StableId
则Remove
相应位置的View,并回收到缓存池中。否则,detach相应位置的View,并回收到临时缓存中
。
remove
是真正将子View从RecyclerView中移除
,而detach
的子View还未和RecycelerView分离
。
所以,如果设置了StableIds()
,将回收到临时缓存中。在复用的时候,会先根据position
在临时缓存中查找,如果还没找到则根据id
来临时缓存中查找。
先来看看移除View时,recycleViewHolderInternal
怎么回收?
void recycleViewHolderInternal(ViewHolder holder) {
// ...省略代码
// 如果CachedViews已经包含这个VH就抛异常
if (DEBUG && mCachedViews.contains(holder)) {
throw new IllegalArgumentException("cached view received recycle internal? "
+ holder + exceptionLabel());
}
if (forceRecycle || holder.isRecyclable()) {
// mViewCacheMax默认为2,并且VH为已经更新或移除
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();
// 移调最前面的0个去RecyclerPool缓存
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;
}
// 将ViewHolder增加到CachedViews
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
// 否则增加到RecycledViewPool中
if (!cached) {
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
}
//...省略代码
}
void recycleCachedViewAt(int cachedViewIndex) {
if (DEBUG) {
Log.d(TAG, "Recycling cached view at index " + cachedViewIndex);
}
ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
if (DEBUG) {
Log.d(TAG, "CachedViewHolder to be recycled: " + viewHolder);
}
// 添加到RecycledViewPool中
addViewHolderToRecycledViewPool(viewHolder, true);
//从CacheViews中移除
mCachedViews.remove(cachedViewIndex);
}
void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {
//...省略代码
if (dispatchRecycled) {
dispatchViewRecycled(holder);
}
holder.mOwnerRecyclerView = null;
getRecycledViewPool().putRecycledView(holder);
}
void dispatchViewRecycled(@NonNull ViewHolder holder) {
if (mRecyclerListener != null) {
mRecyclerListener.onViewRecycled(holder);
}
if (mAdapter != null) {
mAdapter.onViewRecycled(holder);
}
if (mState != null) {
mViewInfoStore.removeViewHolder(holder);
}
if (DEBUG) Log.d(TAG, "dispatchViewRecycled: " + holder);
}
代码中很简单,就是mCacheView
如果满了,就将mCacheView
的第0个移动到RecyclerViewPool
中。然后增加到mCacheView
中。
增加View时,scrapView
怎么回收?
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();
}
holder.setScrapContainer(this, true);
mChangedScrap.add(holder);
}
}
// 有动画
boolean canReuseUpdatedViewHolder(ViewHolder viewHolder) {
return mItemAnimator == null || mItemAnimator.canReuseUpdatedViewHolder(viewHolder,
viewHolder.getUnmodifiedPayloads());
}
大致是没有动画时ViewHolder存在mAttachedScrap
中,否则存在mChangedScrap
中
RecyclerView的复用和回收机制到这里就分析完了。