注册

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

缓存是 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 在滚动中是如何判断哪些表项应该被回收?


上一篇文章中详细分析了列表滚动时,表项是如何被回收的,现援引结论和图示如下。





  1. RecyclerView 在滚动发生之前,会根据预计滚动位移大小来决定需要向列表中填充多少新的表项。在填充表项的同时,也会回收表项,回收的依据是 limit 隐形线




  2. limit 隐形线 是 RecyclerView 在滚动发生之前根据滚动位移计算出来的一条线,它是决定哪些表项该被回收的重要依据。它可以理解为:隐形线当前所在位置,在滚动完成后会和列表顶部重叠。




  3. limit 隐形线 的初始值 = 列表当前可见表项的底部到列表底部的距离,即列表在不填充新表项时,可以滑动的最大距离。每一个新填充表项消耗的像素值都会被追加到 limit 值之上,即limit 隐形线会随着新表项的填充而不断地下移。




  4. 触发回收逻辑时,会遍历当前所有表项,若某表项的底部位于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()scrapview 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 会从三个方面被校验:



  1. 表项是否被移除
  2. 表项 viewType 是否相同
  3. 表项 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 时,scrapview cache中命中的缓存才会被使用。否则即使命中也会视为无效ViewHolder被丢到缓存池中。


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

0 个评论

要回复文章请先登录注册