注册

RecyclerView 面试题 | 哪些情况下表项会被回收到缓存池?(2)

RecyclerView 面试题 | 哪些情况下表项会被回收到缓存池?(1)


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-layoutpost-layoutRecyclerView 动画原理 | 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 是如何存储动画属性值的,现援引如下:





  1. RecyclerView 将表项动画数据封装了两层,依次是ItemHolderInfoInfoRecord,它们记录了列表预布局和后布局表项的位置信息,即表项矩形区域与列表左上角的相对位置,它还用一个int类型的标志位来记录表项经历了哪些布局阶段,以判断表项应该做的动画类型(出现,消失,保持)。




  2. InfoRecord被集中存放在一个商店类ViewInfoStore中。所有参与动画的表项的ViewHolderInfoRecord都会以键值对的形式存储其中。




  3. 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 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_APPEARFLAG_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 阶段也填充到列表中,就都会被回到到缓存池。


作者:唐子玄
链接:https://juejin.cn/post/6930412704578404360/
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册