RecyclerView源码解析(四):RecyclerView对ViewHolder的回收
导言
前面几篇文章我们已经介绍了RecyclerView绘图的三大流程和其四层缓存机制,不过对于我来说还有一个疑问,那就是RecyclerView是如何将分离的ViewHolder进行回收的,最终又回收到了哪一层缓存之中。所以本篇文章就是为了解决这个问题。
创建ViewHolder和回收ViewHolder的方法
在缓存机制中我们提到过了,RecyclerView尝试获取ViewHolder并不是一开始就直接创建一个ViewHolder的,它会首先尝试从它的四层缓存中获取一个可用的ViewHolder,如果没有找到,最后才会调用方法创建一个ViewHolder,而这个方法正是onCreateViewHolder
,也就是我们在适配器中重写的方法。所以每次创建ViewHolder时才会调用该方法。
从我具体测试的结果来看当我们一共有五个数据项的时候RecyclerView只会创建四个ViewHolder,也就是说有一个数据项的显示会通过ViewHolder复用的方式来显示。**也就是说,之前必定有至少一个ViewHolder被回收了。**这个回收过程在哪里呢?我们可以从RecyclerView中可重写的方法中找到突破口。
有以下五个比较重要的回调,下面由我的蹩脚英语进行翻译:
onAttachedToRecyclerView(recyclerView: RecyclerView)
:当RecyclerView开始监听其Adapter时调用。onDetachedFromRecyclerView(recyclerView: RecyclerView)
:当RecyclerView不再监听其Adapter时调用。onViewAttachedToWindow(holder: ViewHolder)
:当由适配器创建的View被添加到Window时调用,这可以被视为是用户即将见到该View的标志。在onViewDetachedFromWindow(holder: ViewHolder)
释放的资源应当在这里被恢复。onViewDetachedFromWindow(holder: ViewHolder)
:当由适配器创建的View被从Window分离时调用,这可能并不是一个永久的状态,RecyclerView可能会选择缓存这些被分离的不可见的View。onViewRecycled(holder: ViewHolder)
:当ViewHolder被RecyclerView回收时调用。当LayoutManager
判断一个View不再需要被绑定在其父RecyclerView时,这个View就会被回收。LayoutManager
这样判断的原因可能是因为该View消失在视线之外或者或者是因为一组被缓存的视图仍然由附加到父 RecyclerView 的视图表示。RecyclerView将在清除Holder内部的数据和将其发送到RecycledViewPool
之前调用该方法。
这五个方法之中显然最后一个方法关联度是最高的,不过其后面的那一句“或者是因为一组被缓存的视图仍然由附加到父 RecyclerView 的视图表示”
的意思可能比较迷惑。在 RecyclerView 中,为了提高性能,通常会维护一个视图池(View Pool),用于存储那些已经创建但当前不可见的视图。这些视图池中的视图仍然附加(attached)到父 RecyclerView,但它们当前可能不可见,因为可能滚动到了屏幕之外或者被遮挡住了。当用户滚动 RecyclerView 时,系统会检查这个视图池,看看是否有可以重用的视图,而不是每次都创建新的视图。这就是所谓的 “缓存的视图”,它们被缓存起来以备将来使用。当需要显示新的数据时,可以从视图池中获取这些缓存的视图,并根据新的数据进行更新,而不需要重新创建整个视图。这样可以显著提高性能,因为重用现有视图比创建新视图要快得多。
因此,这句话的含义是,RecyclerView 中的视图可能不可见,要么因为已经滚动到屏幕之外,要么因为被其他视图遮挡住了,但它们仍然存在于 RecyclerView 的视图池中,可以被重用,以提高性能。
源码分析
分离视图
在上面的回调中onViewDetachedFromWindow(holder: ViewHolder)
是用来分离视图的,所以我们可以从这个方法入手看看RecyclerView中哪里调用了该方法。层层向前推,可以发现这个方法会指向RecyclerView中的一个回调类中:
private final ViewInfoStore.ProcessCallback mViewInfoProcessCallback =
new ViewInfoStore.ProcessCallback() {
@Override
public void processDisappeared(ViewHolder viewHolder, @NonNull ItemHolderInfo info,
@Nullable ItemHolderInfo postInfo) {
mRecycler.unscrapView(viewHolder);
animateDisappearance(viewHolder, info, postInfo);
}
@Override
public void processAppeared(ViewHolder viewHolder,
ItemHolderInfo preInfo, ItemHolderInfo info) {
animateAppearance(viewHolder, preInfo, info);
}
@Override
public void processPersistent(ViewHolder viewHolder,
@NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
viewHolder.setIsRecyclable(false);
if (mDataSetHasChangedAfterLayout) {
// since it was rebound, use change instead as we'll be mapping them from
// stable ids. If stable ids were false, we would not be running any
// animations
if (mItemAnimator.animateChange(viewHolder, viewHolder, preInfo,
postInfo)) {
postAnimationRunner();
}
} else if (mItemAnimator.animatePersistence(viewHolder, preInfo, postInfo)) {
postAnimationRunner();
}
}
@Override
public void unused(ViewHolder viewHolder) {
mLayout.removeAndRecycleView(viewHolder.itemView, mRecycler);
}
};
会指向最后的一个unused(ViewHolder viewHolder)
方法中,这个方法会调用mLayout.removeAndRecycleView(viewHolder.itemView, mRecycler)
方法,最终会触发onViewDetachedFromWindow(holder: ViewHolder)
方法。这个方法最后会在我们之前在布局过程中说过的dispatchLayoutStep3()
中触发,最终触发process
方法:
void process(ProcessCallback callback) {
for (int index = mLayoutHolderMap.size() - 1; index >= 0; index--) {
final RecyclerView.ViewHolder viewHolder = mLayoutHolderMap.keyAt(index);
final InfoRecord record = mLayoutHolderMap.removeAt(index);
if ((record.flags & FLAG_APPEAR_AND_DISAPPEAR) == FLAG_APPEAR_AND_DISAPPEAR) {
// Appeared then disappeared. Not useful for animations.
callback.unused(viewHolder);
} else if ((record.flags & FLAG_DISAPPEARED) != 0) {
// Set as "disappeared" by the LayoutManager (addDisappearingView)
if (record.preInfo == null) {
// similar to appear disappear but happened between different layout passes.
// this can happen when the layout manager is using auto-measure
callback.unused(viewHolder);
} else {
callback.processDisappeared(viewHolder, record.preInfo, record.postInfo);
}
} else if ((record.flags & FLAG_APPEAR_PRE_AND_POST) == FLAG_APPEAR_PRE_AND_POST) {
// Appeared in the layout but not in the adapter (e.g. entered the viewport)
callback.processAppeared(viewHolder, record.preInfo, record.postInfo);
} else if ((record.flags & FLAG_PRE_AND_POST) == FLAG_PRE_AND_POST) {
// Persistent in both passes. Animate persistence
callback.processPersistent(viewHolder, record.preInfo, record.postInfo);
} else if ((record.flags & FLAG_PRE) != 0) {
// Was in pre-layout, never been added to post layout
callback.processDisappeared(viewHolder, record.preInfo, null);
} else if ((record.flags & FLAG_POST) != 0) {
// Was not in pre-layout, been added to post layout
callback.processAppeared(viewHolder, record.preInfo, record.postInfo);
} else if ((record.flags & FLAG_APPEAR) != 0) {
// Scrap view. RecyclerView will handle removing/recycling this.
} else if (DEBUG) {
throw new IllegalStateException("record without any reasonable flag combination:/");
}
InfoRecord.recycle(record);
}
}
这个方法就是通过判断FLAG标志位来决定到底回调哪个方法,可以发现当FLAG的值为FLAG_DISAPPEARED
或者FLAG_APPEAR_AND_DISAPPEAR
时会触发上面的unused
方法,总的来说就是当View消失(DISAPPEARED)时就会触发这个方法。也许你可能会有疑问:“dispatchLayout这一系列方法不是在布局中调用的吗,难道会反复进行布局吗?”实际上dispatchLayout
方法除了在layout中触发之外还会在scrollByInternal
方法中触发,显然这是滑动时会调用到的方法,在onTouchEvent
中执行。所以说,每当我们滑动时会先执行dispatchLayout方法,然后在这其中一旦当前View为不可见状态就会触发unused方法,最后回调到onViewDetachedFromWindow(holder: ViewHolder)
中去。整个调用链如下所示:
回收视图
至于这个分离视图的过程到底做了什么,我们再来继续往前看,之前提到unused
回调,该回调如下所示:
public void unused(ViewHolder viewHolder) {
mLayout.removeAndRecycleView(viewHolder.itemView, mRecycler);
}
可以看到会触发mLayout.removeAndRecyclerView
方法,实际上我们上面给的那张图进行了一定的简化,我们将其详细化,如下:
可以看到unused
方法出发了mLayout.removeAndRecycleView
,这个方法是有默认实现的:
public void removeAndRecycleView(@NonNull View child, @NonNull Recycler recycler) {
removeView(child);
recycler.recycleView(child);
}
可以看到他在触发removeView方法之后还触发了recycler自身的recycleView
方法,不出所料这块应该就是重头戏了,该方法的注释:
注释上也写得很清楚了,该方法是用于回收一个分离的view的,特定的view还会被添加进入view Pool(视图池)中以便之后的重新绑定数据和复用。一个View在被回收之前必须被完全地从父Recycler中分离。当一个View被报废时,它将从scrap List
中被移除。接下来看其方法:
public void recycleView(@NonNull View view) {
// This public recycle method tries to make view recycle-able since layout manager
// intended to recycle this view (e.g. even if it is in scrap or change cache)
ViewHolder holder = getChildViewHolderInt(view);//获得目标View的ViewHolder
if (holder.isTmpDetached()) {//判断该holder是不是暂时分离状态
removeDetachedView(view, false);//移除分离的view,将触发ViewGroup的removeDetachedView方法
}
if (holder.isScrap()) {//判断holder是否有mScrapContainer,这是一个Recycler类
holder.unScrap();//将其从mScrapContainer中移除
} else if (holder.wasReturnedFromScrap()) {
holder.clearReturnedFromScrapFlag();
}
recycleViewHolderInternal(holder);
// In most cases we dont need call endAnimation() because when view is detached,
// ViewPropertyAnimation will end. But if the animation is based on ObjectAnimator or
// if the ItemAnimator uses "pending runnable" and the ViewPropertyAnimation has not
// started yet, the ItemAnimatior on the view may not be cleared.
// In b/73552923, the View is removed by scroll pass while it's waiting in
// the "pending moving" list of DefaultItemAnimator and DefaultItemAnimator later in
// a post runnable, incorrectly performs postDelayed() on the detached view.
// To fix the issue, we issue endAnimation() here to make sure animation of this view
// finishes.
//
// Note the order: we must call endAnimation() after recycleViewHolderInternal()
// to avoid recycle twice. If ViewHolder isRecyclable is false,
// recycleViewHolderInternal() will not recycle it, endAnimation() will reset
// isRecyclable flag and recycle the view.
if (mItemAnimator != null && !holder.isRecyclable()) {
mItemAnimator.endAnimation(holder);
}
}
看起来这个方法大段大段的都是注释,首先开头的这两句的意思是:“当LayoutManager想要回收这个view时,该公共方法将尝试使该view变成可回收的。”上面的holder.isScrap()
判断报废状态以我的理解holder的第一级缓存是mAttachedScrap
,这个mAttachedScrap
是被装在一个Recycler中的,所以holder需要通过这个Recycler才能找到这个mAttachedScrap
,这时候就需要一个变量来指向存储holder的Recycler,这个参数就是ScrapContainer
。当一个ViewHolder被添加进入一个mAttachedScrap
时同时还需要给它设置一个ScrapContainer
来指向存储mAttachedScrap
的Recycler。也就是说这个holder.isScrap()
判断的就是当前holder是否在第一级缓存mAttachedScrap
中的。
好了,继续回到正题接下来会调用recycleViewHolderInternal(holder)
方法进一步回收holder,进入到该方法中:
void recycleViewHolderInternal(ViewHolder holder) {
......
final boolean transientStatePreventsRecycling = holder
.doesTransientStatePreventRecycling();
@SuppressWarnings("unchecked")
final boolean forceRecycle = mAdapter != null //是否强制回收
&& transientStatePreventsRecycling
&& mAdapter.onFailedToRecycleView(holder);
boolean cached = false;
boolean recycled = false;
......
if (forceRecycle || holder.isRecyclable()) { //如果强制回收或者可回收的话
if (mViewCacheMax > 0//判断,如果ViewHolder仍然有效的话
&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_REMOVED
| ViewHolder.FLAG_UPDATE
| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
// Retire oldest cached view
int cachedViewSize = mCachedViews.size();//获得二级缓存CachedView的大小
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) { //如果CachedView的大小已经超出了限制
recycleCachedViewAt(0); //将CachedView中的第一个元素回收
cachedViewSize--;
}
int targetCacheIndex = cachedViewSize; //CachedView中的目标下标,也就是最后一位
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);//将holder添加进入二级缓存中
cached = true;//将cached标志位置为true,代表该holder成功添加入二级缓存中
}
if (!cached) {//如果缓存进一级缓存时失败
addViewHolderToRecycledViewPool(holder, true);//将其添加进入RecycledViewPool四级缓存中
recycled = 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;//说明当前holder已经没有宿主RecyclerView了
}
}
上面方法中重要的部分我都已经注释出来了简单来说就是先尝试将回收的holder加入二级缓存mCachedView
中,如果mCachedView
已满就将第一个元素移除再添加。如果缓存二级缓存失败的话再尝试调用addViewHolderToRecycledViewPool
尝试将其加入到四级缓存中。
说到现在我们发现这个回收方法只涉及到了第二级缓存和第四级缓存,除去我们自定义的第三级缓存,那第一级缓存去哪里了?实际上,具体将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<ViewHolder>();
}
holder.setScrapContainer(this, true);
mChangedScrap.add(holder);
}
}
这个方法向前追踪的话是指向了getScrapOrHiddenOrCachedHolderForPosition
,这个方法也是在布局过程中提到的:
ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
final int scrapCount = mAttachedScrap.size();
//尝试从第一级缓存中获取holder,如果获取成功就直接返回
........
//若没有在第一级缓存中找到holder
if (!dryRun) {
View view = mChildHelper.findHiddenNonRemovedView(position);//找到隐藏的且没有被移除的View
if (view != null) {//如果View不为空
// This View is good to be used. We just need to unhide, detach and move to the
// scrap list.
final ViewHolder vh = getChildViewHolderInt(view);//将其包装成一个ViewHolder
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);//将其从父RecylerView中分离
scrapView(view);//将其添加进入第一级缓存中
vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP
| ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
return vh;
}
}
..........
}
return null;
}
这里我们也截取最重要的部分并且我已经加入了部分注释了。可以看到这所有的第一级缓存中的ViewHolder的来源都是一个叫做mHiddenViews
中的List中获取的。这个mHiddenViews
中的View按照我的理解都是已经不可见但是仍未从被从RecyclerView分离的View的列表。所以RecyclerView将ViewHolder回收进这三级缓存的逻辑已经很清楚了。
总结
我们最后来总结一下RecyclerView回收ViewHolder的逻辑,首先RecyclerView尝试填充内容时会先尝试从这四级缓存中获取可用的ViewHolder,首先就是从第一级缓存mScrapViews
中查找,如果找到了就直接返回;如果第一级缓存中没有可用的Holder,那么接下来RecyclerView还会额外查看是否有不可见但是仍然附加在RecyclerView的View,如果有的话就将其包装成Holder添加进入第一级缓存中,并将其从之前的父RecyclerView中分离,这就是第一级缓存的来源。
之后在RecyclerView的滑动过程中又有一个新的View不可见的话就会触发新的回调,该回调中首先会判断当前的View是否已经被第一级缓存存储了,如果已经被第一级缓存存储就直接返回;否则就会先尝试将其回收进入第二级缓存中,当第二级缓存中缓存失败时又会尝试进行第四级缓存;至于第三级缓存是我们自定义的,一般也不用。
最后上一张总结图: