缓存是 RecyclerView 时间性能优越的重要原因。缓存池是所有缓存中速度最慢的,其中的ViewHodler
是脏的,得重新执行onBindViewHolder()
。这一篇从源码出发,探究哪些情况下“表项会被回收到缓存池”。
缓存池结构
在分析不同的回收场景前,先回顾一下“缓存池是什么?”
表项被回收到缓存池,在源码上的表项为 ViewHolder 实例被存储到RecycledViewPool
结构中:
public class RecyclerView { public final class Recycler { // 回收表项视图 public void recycleView(@NonNull View view) { ViewHolder holder = getChildViewHolderInt(view); // 回收表项 ViewHolder recycleViewHolderInternal(holder); } // 回收 ViewHolder void recycleViewHolderInternal(ViewHolder holder) { ... // 将 ViewHolder 存入缓存池 addViewHolderToRecycledViewPool(holder, true); } // 将 ViewHolder 实例存储到 RecycledViewPool 结构中 void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) { ... getRecycledViewPool().putRecycledView(holder); } // 获取 RecycledViewPool 实例 RecycledViewPool getRecycledViewPool() { if (mRecyclerPool == null) { mRecyclerPool = new RecycledViewPool(); } return mRecyclerPool; } } // 缓存池 public static class RecycledViewPool { // 单类型缓存列表 static class ScrapData { final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>(); } // 多类型缓存列表构成的缓存池(以 int 为键) SparseArray<ScrapData> mScrap = new SparseArray<>(); public void putRecycledView(ViewHolder scrap) { // 获取 ViewHolder 类型 final int viewType = scrap.getItemViewType(); // 获取指定类型的 ViewHolder 缓存列表 final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap; ... // ViewHolder 实例存入缓存列表 scrapHeap.add(scrap); } } }
RecycledViewPool
用一个SparseArray
将不同类型的 ViewHolder 实例缓存在内存,每种类型对应一个列表。当有相同类型的表项插入列表时,不用重新创建 ViewHolder 实例(执行 onCreateViewHolder()),从缓存池中获取即可。
关于缓存池的详细解析可以点击RecyclerView 缓存机制 | 回收到哪去?
1. 表项主动移出屏幕
这种回收表项的场景是最常见的。效果图如下:
为啥要等 item 3 滚出屏幕后,item 1 才刚刚被回收,而 item 4 滚出屏幕后,item 2 立马被回收了?
这是因为mCachedViews
的存在,它是默认大小为 2 的列表。用于缓存移出屏幕表项的 ViewHolder。
所有移出的表项都会依次被缓存至其中,当mCachedViews
满时,按照先进先出原则,将最先存入的 ViewHolder 实例移除并转存至RecycledViewPool
,即缓存池中。
所以 item 1 和 2 移出屏幕时,正好填满mCachedViews
,当 item 3 移出屏幕时,item 1 就被挤出并存入缓存池。更详细的源码跟踪分析可以点击RecyclerView 缓存机制 | 回收到哪去?
那 RecyclerView 在滚动中是如何判断哪些表项应该被回收?
在上一篇文章中详细分析了列表滚动时,表项是如何被回收的,现援引结论和图示如下。
- RecyclerView 在滚动发生之前,会根据预计滚动位移大小来决定需要向列表中填充多少新的表项。在填充表项的同时,也会回收表项,回收的依据是 limit 隐形线。
- limit 隐形线 是 RecyclerView 在滚动发生之前根据滚动位移计算出来的一条线,它是决定哪些表项该被回收的重要依据。它可以理解为:隐形线当前所在位置,在滚动完成后会和列表顶部重叠。
- limit 隐形线 的初始值 = 列表当前可见表项的底部到列表底部的距离,即列表在不填充新表项时,可以滑动的最大距离。每一个新填充表项消耗的像素值都会被追加到 limit 值之上,即limit 隐形线会随着新表项的填充而不断地下移。
- 触发回收逻辑时,会遍历当前所有表项,若某表项的底部位于limit 隐形线下方,则该表项上方的所有表项都会被回收。
下图形象地描述了 limit 隐形线(图中红色虚线):
回收逻辑落实在源码上,就是如下(0-5)的调用链:
public class RecyclerView { public final class Recycler { // 5 public void recycleView(View view) {...} } public abstract static class LayoutManager { public void removeAndRecycleViewAt(int index, @NonNull Recycler recycler) { final View view = getChildAt(index); removeViewAt(index); // 4 recycler.recycleView(view); } } } public class LinearLayoutManager { private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) { // 3:回收索引值为 endIndex -1 到 startIndex 的表项 for (int i = endIndex - 1; i >= startIndex; i--) { removeAndRecycleViewAt(i, recycler); } } private void recycleViewsFromStart(RecyclerView.Recycler recycler, int scrollingOffset,int noRecycleSpace) { ... for (int i = 0; i < childCount; i++) { View child = getChildAt(i); if (mOrientationHelper.getDecoratedEnd(child) > limit|| mOrientationHelper.getTransformedEndWithDecoration(child) > limit) { // 2 recycleChildren(recycler, 0, i); } } } private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) { // 1 recycleViewsFromStart(recycler, scrollingOffset, noRecycleSpace); } int fill(RecyclerView.Recycler recycler, LayoutState layoutState,RecyclerView.State state, boolean stopOnFocusable) { ... // 循环填充表项 while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) { // 填充单个表项 layoutChunk(recycler, state, layoutState, layoutChunkResult); ... if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) { layoutState.mScrollingOffset += layoutChunkResult.mConsumed; // 0:回收表项 recycleByLayoutState(recycler, layoutState); } ... } } }
每填充一个表项都会遍历已加载的所有表项,以检测其中是否有可以回收的。
若对结论的源码分析过程感兴趣,可以点击RecyclerView 面试题 | 滚动时表项是如何被填充或回收的?
2. 表项被挤出屏幕
当列表中有表项插入,把现有表项挤出屏幕时,也会发生表项回收。效果图如下:
这种场景下 item 2 会被回收,当表项动画完成后,就会触发表项回收逻辑:
// RecyclerView 默认表项动画器 public class DefaultItemAnimator extends SimpleItemAnimator { // 启动表项位移动画 void animateMoveImpl(final RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) { final ViewPropertyAnimator animation = view.animate(); animation.setDuration(getMoveDuration()).setListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animator) { // 往上分发动画结束事件 dispatchMoveFinished(holder); ... } }).start(); } } public abstract class SimpleItemAnimator extends RecyclerView.ItemAnimator { public final void dispatchMoveFinished(RecyclerView.ViewHolder item) { // 继续往上分发动画结束事件 dispatchAnimationFinished(item); } } public class RecyclerView { public abstract static class ItemAnimator { private ItemAnimatorListener mListener = null; public final void dispatchAnimationFinished(ViewHolder viewHolder) { // 将动画结束事件分发给监听器 if (mListener != null) { mListener.onAnimationFinished(viewHolder); } } } private class ItemAnimatorRestoreListener implements ItemAnimator.ItemAnimatorListener { @Override public void onAnimationFinished(ViewHolder item) { // 设置 ViewHolder 为可回收的 item.setIsRecyclable(true); // 回收表项 if (!removeAnimatingView(item.itemView) && item.isTmpDetached()) { removeDetachedView(item.itemView, false); } } } boolean removeAnimatingView(View view) { startInterceptRequestLayout(); final boolean removed = mChildHelper.removeViewIfHidden(view); // 当表项做完位移动画后确实移出了屏幕 if (removed) { final ViewHolder viewHolder = getChildViewHolderInt(view); mRecycler.unscrapView(viewHolder); // 回收 ViewHolder mRecycler.recycleViewHolderInternal(viewHolder); } ... return removed; } }
RecyclerView 的表项动画器将移动表项动画的结束事件层层传递,最终传递到了 RecyclerView 内部的监听器,由监听器通知 Recycler 触发表项回收动作。
3. 高速缓存命中的 ViewHolder 变脏
变脏的意思是表项需要重绘,即调用onBindViewHolder()
重新为表项绑定数据。
RecyclerView 中有四级缓存,它会优先去高速缓存中找 ViewHolder 实例。缓存池是其中速度最慢的,因为从中取出的 ViewHolder 需要重新执行onBindViewHolder()
。scrap
和view cache
的速度都比它快,但命中后需要进行额外的校验(关于四级缓存的详解可以点击这里):
public class RecyclerView public final class Recycler { // RecyclerView 获取 ViewHolder 的入口 ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) { // 从 scrap 或 view cache 中获取 ViewHolder 实例 holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun); // 若缓存命中 if (holder != null) { // 校验 ViewHolder if (!validateViewHolderForOffsetPosition(holder)) { // 校验失败 if (!dryRun) {// dryRun 始终为 false .... // 回收命中的 ViewHolder (丢到缓存池) recycleViewHolderInternal(holder); } // 标记从 scrap 或 view cache 中获取缓存失败 // 会触发从其他缓存继续获取 ViewHolder实例 holder = null; } else { // 标记校验成功 fromScrapOrHiddenOrCache = true; } } .... } } }
从 scrap 或 view cache 命中的 ViewHolder 会从三个方面被校验:
- 表项是否被移除
- 表项 viewType 是否相同
- 表项 id 是否相同
public class RecyclerView{ public final class Recycler { // 校验 ViewHolder 合法性 boolean validateViewHolderForOffsetPosition(ViewHolder holder) { // 如果表项已被移除 if (holder.isRemoved()) { // 是否在 preLayout 阶段 return mState.isPreLayout(); } if (!mState.isPreLayout()) { // 检查从缓存中获取的 ViewHolder 是否和 Adapter 对应位置的 ViewHolder 有相同的 viewType final int type = mAdapter.getItemViewType(holder.mPosition); if (type != holder.getItemViewType()) { return false; } } // 检查从缓存中获取的 ViewHolder 是否和 Adapter 对应位置的 ViewHolder 有相同的 id if (mAdapter.hasStableIds()) { return holder.getItemId() == mAdapter.getItemId(holder.mPosition); } return true; } } }
只有和指定位置表项具有相同的 viewType 或相同的 id 时,scrap
和view cache
中命中的缓存才会被使用。否则即使命中也会视为无效ViewHolder
被丢到缓存池中。
4. mCachedViews 中缓存的表项被删除
表项移出屏幕后,立刻被回收到mCachedViews
结构中。若恰巧该表项又被删除了,则表项对应的 ViewHolder 从mCachedViews
结构中移除,并添加到缓存池中:
public class RecyclerView { public final class Recycler { void recycleCachedViewAt(int cachedViewIndex) { // 从 mCacheViews 结构中获取指定位置的 ViewHolder 实例 ViewHolder viewHolder = mCachedViews.get(cachedViewIndex); // 将 ViewHolder 存入缓存池 addViewHolderToRecycledViewPool(viewHolder, true); // 将 ViewHolder 从 mCacheViews 中移除 mCachedViews.remove(cachedViewIndex); } void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) { ... getRecycledViewPool().putRecycledView(holder); } } }
5. pre-layout 中额外填充的表项在 post-layout 中被移除
pre-layout & post-layout
pre-layout
和post-layout
在RecyclerView 动画原理 | pre-layout,post-layout 与 scrap 缓存的关系有介绍过,援引如下:
RecyclerView 要做表项动画,
为了确定动画的种类和起终点,需要比对动画前和动画后的两张“表项快照”
为了获得两张快照,就得布局两次,分别是 pre-layout 和 post-layout(布局即是往列表中填充表项),
为了让两次布局互不影响,就不得不在每次布局前先清除上一次布局的内容(就好比先清除画布,重新作画),
但是两次布局中所需的某些表项大概率是一摸一样的,若在清除画布时,把表项的所有信息都一并清除,那重新作画时就会花费更多时间(重新创建 ViewHolder 并绑定数据),
RecyclerView 采取了用空间换时间的做法:在清除画布时把表项缓存在 scrap 缓存中,以便在填充表项可以命中缓存,以缩短填充表项耗时。
Gif 的场景中,在 pre-layout 阶段,item 1、item 2、item 3 被填充到列表中,形成一张动画前的表项快照。而 post-layout 将 item 1、item 3 填充到列表中,形成一张动画后的表项快照。
对比这两张快照中的 item 3 的位置就能知道它该从哪里平移到哪里,也知道 item 2 需要做消失动画,当动画结束后,item 2 的 ViewHolder 会被回收到缓存池,回收的调用链和“表项被挤出屏幕”是一样的,都是由动画结束来触发的。
在 pre-layout 阶段填充额外表项
考虑另外一种场景,这次不是移除 item 2,而是更新它,比如把 item 2 更新成 item 2.1,那 pre-layout 还会将 item 3 填充进列表吗?
RecyclerView 动画原理 | 换个姿势看源码(pre-layout) 详细分析了,在 pre-layout 阶段,额外的表项是如何被填充到列表,其中关键源码再拿出来看一下:
public class LinearLayoutManager{ // 向列表中填充表项 int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) { ... // 计算剩余空间 int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace; // 循环填充表项,直到没有剩余空间 while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) { layoutChunkResult.resetInternal(); // 填充单个表项 layoutChunk(recycler, state, layoutState, layoutChunkResult); ... // 在列表剩余空间中扣除刚填充表项所消耗的空间 if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null || !state.isPreLayout()) { layoutState.mAvailable -= layoutChunkResult.mConsumed; remainingSpace -= layoutChunkResult.mConsumed; } ... } ... } }
直觉上,每填充一个表项都应该将其消耗的空间扣除,但扣除逻辑套在了一个 if 中,即扣除是有条件的。
条件表达式中一共有三个条件,在预布局阶段!state.isPreLayout()
必然是 false,layoutState.mScrapList != null
也是 false(断点告诉我的),最后一个条件!layoutChunkResult.mIgnoreConsumed
起了决定性的作用,它在填充单个表项时被赋值:
public class LinearLayoutManager { void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,LayoutState layoutState, LayoutChunkResult result) { // 获取下一个该被填充的表项视图 View view = layoutState.next(recycler); ...// 省略了实施填充的具体逻辑 // 如果表项被移除或被更新 则 mIgnoreConsumed 置为 true if (params.isItemRemoved() || params.isItemChanged()) { result.mIgnoreConsumed = true; } ... } }
layoutChunkResult
被作为参数传入layoutChunk()
,并且当填充表项是被删除的或是被更新的,就将layoutChunkResult.mIgnoreConsumed
置为 true。表示该表项虽然被填充进了列表但是它占用的空间应该呗忽略。至此可以得出结论:
在预布局阶段,循环填充表项时,若遇到被移除的或是被更新的表项,则会忽略它占用的空间,多余空间被用来加载额外的表项,这些表项在屏幕之外,本来不会被加载。
虽然这结论就是代码的本意,但还是有一点让我不太明白。忽略被移除表项占用的空间容易理解,那为啥更新的表项也一同被忽略?
那是因为,更新表项时,表项的布局可能发生变化(取决于onBindViewHolder()的实现),万一表项布局变长,则会造成其他表项被挤出屏幕,或是表项变短,造成新表项移入屏幕。
记录表项动画信息
在RecyclerView 动画原理 | 如何存储并应用动画属性值?中介绍了 RecyclerView 是如何存储动画属性值的,现援引如下:
- RecyclerView 将表项动画数据封装了两层,依次是
ItemHolderInfo
和InfoRecord
,它们记录了列表预布局和后布局表项的位置信息,即表项矩形区域与列表左上角的相对位置,它还用一个int
类型的标志位来记录表项经历了哪些布局阶段,以判断表项应该做的动画类型(出现,消失,保持)。
InfoRecord
被集中存放在一个商店类ViewInfoStore
中。所有参与动画的表项的ViewHolder
与InfoRecord
都会以键值对的形式存储其中。
- RecyclerView 在布局的第三阶段会遍历商店类中所有的键值对,以
InfoRecord
中的标志位为依据,判断执行哪种动画。表项预布局和后布局的位置信息会一并传递给RecyclerView.ItemAnimator
,以触发动画。
在 pre-layout 阶段,存储动画信息的代码如下:
public class RecyclerView { private void dispatchLayoutStep1() { ... // 遍历列表中现有表项 int count = mChildHelper.getChildCount(); for (int i = 0; i < count; ++i) { final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i)); // 为表项构建 ItemHolderInfo 实例 final ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation(mState, holder, ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),holder.getUnmodifiedPayloads()); // 将 ItemHolderInfo 实例存入 ViewInfoStore mViewInfoStore.addToPreLayout(holder, animationInfo); } ... // 预布局 mLayout.onLayoutChildren(mRecycler, mState); // 预布局后,再次遍历所有孩子(预布局可能填充额外的表项) for (int i = 0; i < mChildHelper.getChildCount(); ++i) { final View child = mChildHelper.getChildAt(i); final ViewHolder viewHolder = getChildViewHolderInt(child); // 过滤掉带有 FLAG_PRE 标志位的表项 if (!mViewInfoStore.isInPreLayout(viewHolder)) { // 为额外填充的表项构建 ItemHolderInfo 实例 final ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation(mState, viewHolder, flags, viewHolder.getUnmodifiedPayloads()); // 将 ItemHolderInfo 实例存入 ViewInfoStore mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo); } } ... } } class ViewInfoStore { void addToPreLayout(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) { InfoRecord record = mLayoutHolderMap.get(holder); if (record == null) { record = InfoRecord.obtain(); mLayoutHolderMap.put(holder, record); } record.preInfo = info; // 添加 FLAG_PRE 标志位 record.flags |= FLAG_PRE; } void addToAppearedInPreLayoutHolders(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) { InfoRecord record = mLayoutHolderMap.get(holder); if (record == null) { record = InfoRecord.obtain(); mLayoutHolderMap.put(holder, record); } // 添加 FLAG_APPEAR 标志位 record.flags |= FLAG_APPEAR; record.preInfo = info; } }
在 pre-layout 的前后,遍历了两次表项。
对于 Demo 的场景来说,第一次遍历,item 1 和 2 的动画属性被存入 ViewInfoStore 并添加了FLAG_PRE
标志位。遍历结束后执行预布局,把屏幕之外的 item 3 也填充到列表中。再紧接着的第二次遍历中,item 3 的动画属性也会被存入 ViewInfoStore 并添加了FLAG_APPEAR
标志位,表示该表项是在预布局过程中额外被填充的。
在 post-layout 阶段,为了形成动画后的表项快照,得清空列表,重新填充表项,出于时间性能的考虑,被移除表项的 ViewHolder 缓存到了 scrap 结构中(item 1 2 3的 ViewHodler 实例)。
重新向列表中填充 item 1 和更新后的 item 2,它们的 ViewHolder 实例可以从 scrap 结构中快速获取,不必再执行 onCreateViewHolder()。填充完后,列表的空间已经用完,而 scrap 结构中还剩一个 item 3 的 ViewHolder 实例。它会在 post-layout 阶段被添加新的标志位:
public class LinearLayoutManager { // 在 dispatchLayoutStep2() 中第二次调用 onLayoutChildren() 进行 post-layout public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { ... // 为动画而进行布局 layoutForPredictiveAnimations(recycler, state, startOffset, endOffset); } private void layoutForPredictiveAnimations(RecyclerView.Recycler recycler,RecyclerView.State state, int startOffset,int endOffset) { final List<RecyclerView.ViewHolder> scrapList = recycler.getScrapList(); final int scrapSize = scrapList.size(); // 遍历 scrap 结构 for (int i = 0; i < scrapSize; i++) { RecyclerView.ViewHolder scrap = scrapList.get(i); final int position = scrap.getLayoutPosition(); final int direction = position < firstChildPos != mShouldReverseLayout? LayoutState.LAYOUT_START : LayoutState.LAYOUT_END; // 计算 scrap 结构中对应表项所占用的空间 if (direction == LayoutState.LAYOUT_START) { scrapExtraStart += mOrientationHelper.getDecoratedMeasurement(scrap.itemView); } else { scrapExtraEnd += mOrientationHelper.getDecoratedMeasurement(scrap.itemView); } } // mLayoutState.mScrapList 被赋值 mLayoutState.mScrapList = scrapList; // 再次尝试填充表项 if (scrapExtraStart > 0) { ... fill(recycler, mLayoutState, state, false); } if (scrapExtraEnd > 0) { ... fill(recycler, mLayoutState, state, false); } mLayoutState.mScrapList = null; } void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) { View view = layoutState.next(recycler); RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams(); // 分支1:把表项填充到列表中 if (layoutState.mScrapList == null) { if (mShouldReverseLayout == (layoutState.mLayoutDirection == LayoutState.LAYOUT_START)) { addView(view); } else { addView(view, 0); } } // 分支2:把表项动画信息存储到 ViewInfoStore 中 else { if (mShouldReverseLayout == (layoutState.mLayoutDirection == LayoutState.LAYOUT_START)) { // 委托给父类 LayoutManger addDisappearingView(view); } else { addDisappearingView(view, 0); } } ... } }
这次填充表项的layoutChunk()
因为layoutState.mScrapList
不为空,会走不一样的分支,即调用addDisappearingView()
:
public class RecyclerView { public abstract static class LayoutManager { public void addDisappearingView(View child) { addDisappearingView(child, -1); } public void addDisappearingView(View child, int index) { addViewInt(child, index, true); } private void addViewInt(View child, int index, boolean disappearing) { final ViewHolder holder = getChildViewHolderInt(child); if (disappearing || holder.isRemoved()) { // 置 FLAG_DISAPPEARED 标志位 mRecyclerView.mViewInfoStore.addToDisappearedInLayout(holder); } else { mRecyclerView.mViewInfoStore.removeFromDisappearedInLayout(holder); } ... } } } class ViewInfoStore { // 置 FLAG_DISAPPEARED 标志位 void addToDisappearedInLayout(RecyclerView.ViewHolder holder) { InfoRecord record = mLayoutHolderMap.get(holder); if (record == null) { record = InfoRecord.obtain(); mLayoutHolderMap.put(holder, record); } record.flags |= FLAG_DISAPPEARED; } 」
至此 item 3 在经历了 pre-layout 和 post-layout 后,它的动画信息被存储在ViewInfoStore
中,且添加了两个标志位,分别是FLAG_APPEAR
和FLAG_DISAPPEARED
。
在布局的第三阶段,会调用ViewInfoStore.process()
触发动画:
public class RecyclerView { private void dispatchLayoutStep3() { ... // 触发表项执行动画 mViewInfoStore.process(mViewInfoProcessCallback); ... } } class ViewInfoStore { void process(ProcessCallback callback) { // 遍历所有参与动画表项的位置信息 for (int index = mLayoutHolderMap.size() - 1; index >= 0; index--) { // 获取表项 ViewHolder final RecyclerView.ViewHolder viewHolder = mLayoutHolderMap.keyAt(index); // 获取与 ViewHolder 对应的动画信息 final InfoRecord record = mLayoutHolderMap.removeAt(index); // 根据动画信息的标志位确定动画类型以执行对应的 ProcessCallback 回调 if ((record.flags & FLAG_APPEAR_AND_DISAPPEAR) == FLAG_APPEAR_AND_DISAPPEAR) { callback.unused(viewHolder); } else if ((record.flags & FLAG_DISAPPEARED) != 0) { ... } } } }
Demo 中的 item 3 会命中第一个 if 条件,因为:
class ViewInfoStore { static class InfoRecord { // 在 post-layout 中消失 static final int FLAG_DISAPPEARED = 1; // 在 pre-layout 中出现 static final int FLAG_APPEAR = 1 << 1; // 上两者的合体 static final int FLAG_APPEAR_AND_DISAPPEAR = FLAG_APPEAR | FLAG_DISAPPEARED; } }
回收 item 3 到缓存池的逻辑就在callback.unused(viewHolder)
中:
public class RecyclerView { private final ViewInfoStore.ProcessCallback mViewInfoProcessCallback = new ViewInfoStore.ProcessCallback() { ... @Override public void unused(ViewHolder viewHolder) { // 回收没有用的表项 mLayout.removeAndRecycleView(viewHolder.itemView, mRecycler); } }; public abstract static class LayoutManager { public void removeAndRecycleView(@NonNull View child, @NonNull Recycler recycler) { removeView(child); // 委托给 Recycler recycler.recycleView(child); } } public final class Recycler { public void recycleView(@NonNull View view) { // 回收表项到缓存池 recycleViewHolderInternal() } } }
至此可以得出结论:
所有在 pre-layout 阶段被额外填充的表项,若最终没能在 post-layout 阶段也填充到列表中,就都会被回到到缓存池。
推荐阅读
RecyclerView 系列文章目录如下: