缓存是 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 系列文章目录如下:
- RecyclerView 缓存机制 | 如何复用表项?
- RecyclerView 缓存机制 | 回收些什么?
- RecyclerView 缓存机制 | 回收到哪去?
- RecyclerView缓存机制 | scrap view 的生命周期
- 读源码长知识 | 更好的RecyclerView点击监听器
- 代理模式应用 | 每当为 RecyclerView 新增类型时就很抓狂
- 更好的 RecyclerView 表项子控件点击监听器
- 更高效地刷新 RecyclerView | DiffUtil二次封装
- 换一个思路,超简单的RecyclerView预加载
- RecyclerView 动画原理 | 换个姿势看源码(pre-layout)
- RecyclerView 动画原理 | pre-layout,post-layout 与 scrap 缓存的关系
- RecyclerView 动画原理 | 如何存储并应用动画属性值?
- RecyclerView 面试题 | 列表滚动时,表项是如何被填充或回收的?
- RecyclerView 面试题 | 哪些情况下表项会被回收到缓存池?
- RecyclerView 性能优化 | 把加载表项耗时减半 (一)
- RecyclerView 性能优化 | 把加载表项耗时减半 (二)
- RecyclerView 性能优化 | 把加载表项耗时减半 (三)
- RecyclerView 的滚动是怎么实现的?(一)| 解锁阅读源码新姿势
- RecyclerView 的滚动时怎么实现的?(二)| Fling