【Android】RecyclerView 缓存机制,真的很难理解?到底是几级缓存?

我们将以场景化的方式,讲解 RecyclerView 的缓存机制。常见的两个场景是:

1.滑动 RecyclerView 下的缓存机制

2.RecyclerView 初次加载过程的缓存机制

本文将讲解 滑动 RecyclerView 下 的缓存机制


背景知识:负责回收和复用 ViewHolder 的类是 Recycler,负责缓存的主要就是这个类的几个成员变量。我们贴点源码看看(下面源码的注释(和我写的注释),很重要,要记得认真看哦)

/**
 * A Recycler is responsible for managing scrapped or detached item views for reuse.
 * A "scrapped" view is a view that is still attached to its parent RecyclerView but that has been marked for removal or reuse.
 * 
 * Typical use of a Recycler by a RecyclerView.LayoutManager will be to obtain views 
 * for an adapter's data set representing the data at a given position or item ID. 
 * If the view to be reused is considered "dirty" the adapter will be asked to rebind it.
 * If not, the view can be quickly reused by the LayoutManager with no further work. 
 * Clean views that have not requested layout may be repositioned by a LayoutManager without remeasurement.
 */
public final class Recycler {
    final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();// 存放可见范围内的 ViewHolder (但是在 onLayoutChildren 的时候,会将所有 View 都会缓存到这), 从这里复用的 ViewHolder 如果 position 或者 id 对应的上,则不需要重新绑定数据。
    ArrayList<ViewHolder> mChangedScrap = null;// 存放可见范围内并且数据发生了变化的 ViewHolder,从这里复用的 ViewHolder 需要重新绑定数据。

    final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>(); // 存放 remove 掉的 ViewHolder,从这里复用的 ViewHolder 如果 position 或者 id 对应的上,则不需要重新绑定数据。

    private int mRequestedCacheMax = DEFAULT_CACHE_SIZE; // 默认值是 2
    int mViewCacheMax = DEFAULT_CACHE_SIZE; // 默认值是 2

    RecycledViewPool mRecyclerPool; // 存放 remove 掉,并且重置了数据的 ViewHolder,从这里复用的 ViewHolder 需要重新绑定数据。 // 默认值大小是 5 

    private ViewCacheExtension mViewCacheExtension; // 自定义的缓存
    }

至于到底有几级缓存,我觉得这个问题不大重要。有人说三层,有人说四层。有人说三层,因为觉得自定义那层,不是 RecyclerView 实现的,所以不算;也有人认为 Scrap 并不是真正的缓存,所以不算。

从源码看来,我更同意后者,Scrap 不算一层缓存。因为在源码中,mCachedViews 被称为 first-level。至于为什么 Scrap 不算一层,我的理解是:因为这层的只是 detach 了,并没有 remove,所以这层也没有缓存大小的概念,只要符合规则就会加入进去。

// Search the first-level cache
final int cacheSize = mCachedViews.size();

类型

变量名

存储说明

备注

Scrap

mAttachedScrap

存放可见范围内的 ViewHolder

从这里复用的 ViewHolder 如果 position 或者 id 对应的上,则不需要重新绑定数据。

在 onLayoutChildren 的时候,会将所有 View 都会缓存到这

mChangedScrap

存放可见范围内并且数据发生了变化的 ViewHolder

从这里复用的 ViewHolder 需要重新绑定数据。

Cache

mCachedViews

存放 remove 掉的 ViewHolder

从这里复用的 ViewHolder 如果 position 或者 id 对应的上,则不需要重新绑定数据。

ViewCacheExtension

mViewCacheExtension

自定义缓存

RecycledViewPool

mRecyclerPool

存放 remove 掉,并且重置了数据的 ViewHolder

从这里复用的 ViewHolder 需要重新绑定数据。

场景一:滑动 RecyclerView

通过 Android Studio 的 Profiles 工具,我们可以看到调用流程

 入口是 ouTouchEvent

通过表格的方式,简要说明上图的流程都在做什么?

方法名

隶属的类

作用描述

onTouchEvent()

RecyclerView

处理点击事件,在 MOVE 事件中在一定条件下,拦截事件后,做事件处理

scrollByInternal()

RecyclerView

主要是调用 scrollStep()

scrollStep()

RecyclerView

通过 dx 和 dy 的值判断是调用

scrollHorizontallyBy()

还是 scrollVerticallyBy()

scrollHorizontallyBy()

/scrollVerticallyBy()

LayoutManager

主要是调用 scrollBy()

scrollBy()

LayoutManager

通过调用 fill() 添加滑进来的View 和回收滑出去的 View

offsetChildrenVertical()/

offsetChildrenHorizontal()

RecyclerView

做偏移操作

通过上述表格,我们知道了。最重要的东西那就是 scrollBy 中调用了 fill 的方法了。那我们看看 fill 在做什么吧?滑出去的 View 最后去哪里了呢?滑进来的 View 是怎么来的?(带着这个问题,我们一起来读源码!一定要带着),源码只留下了核心部分

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
        RecyclerView.State state, boolean stopOnFocusable) {
    // max offset we should set is mFastScroll + available
    final int start = layoutState.mAvailable;
    //首选该语句块的判断,判断当前状态是否为滚动状态,如果是的话,则触发 recycleByLayoutState 方法
    if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
        // TODO ugly bug fix. should not happen
        if (layoutState.mAvailable < 0) {
            layoutState.mScrollingOffset += layoutState.mAvailable;
        }
        // 分析1----回收
        recycleByLayoutState(recycler, layoutState);
        }
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
        //分析2----复用
        layoutChunk(recycler, state, layoutState, layoutChunkResult);
    }
}
// 分析1----回收 
// 通过一步步追踪,我们发现最后调用的是 removeAndRecycleViewAt() 
public void removeAndRecycleViewAt(int index, @NonNull Recycler recycler) {
    final View view = getChildAt(index);
    //分析1-1
    removeViewAt(index);
    //分析1-2
    recycler.recycleView(view);
}
// 分析1-1
// 从 RecyclerView 移除一个 View 
public void removeViewAt(int index) {
    final View child = getChildAt(index);
    if (child != null) {
        mChildHelper.removeViewAt(index);
    }
}
//分析1-2 
// recycler.recycleView(view) 最终调用的是 recycleViewHolderInternal(holder) 进行回收 VH (ViewHolder)
void recycleViewHolderInternal(ViewHolder holder) {
    if (forceRecycle || holder.isRecyclable()) {
        //判断是否满足放进 mCachedViews 
        if (mViewCacheMax > 0 && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID| ViewHolder.FLAG_REMOVED| ViewHolder.FLAG_UPDATE| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)){
            // 判断 mCachedViews 是否已满
            if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                // 如果满了就将下标为0(即最早加入的)移除,同时将其加入到 RecyclerPool 中
                recycleCachedViewAt(0);
                cachedViewSize--;
                }  
            mCachedViews.add(targetCacheIndex, holder);
            cached = true;
            }
        //如果没有满足上面的条件,则直接存进 RecyclerPool 中    
        if (!cached) {
            addViewHolderToRecycledViewPool(holder, true);
            recycled = true;
         } 
     }
}
//分析2
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
        LayoutState layoutState, LayoutChunkResult result) {
    //分析2-1
    View view = layoutState.next(recycler);
    if (layoutState.mScrapList == null) {
        if (mShouldReverseLayout == (layoutState.mLayoutDirection
                == LayoutState.LAYOUT_START)) {
            //添加到 RecyclerView 上
            addView(view);
        } else {
            addView(view, 0);
        }
    }
}
//分析2-1
//layoutState.next(recycler) 最后调用的是 tryGetViewHolderForPositionByDeadline() 这个方法正是 复用 核心的方法
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
        boolean dryRun, long deadlineNs) {
    // 0) If there is a changed scrap, try to find from there
    // 例如:我们调用 notifyItemChanged 方法时
    if (mState.isPreLayout()) {
        // 如果是 changed 的 ViewHolder 那么就先从 mChangedScrap 中找
        holder = getChangedScrapViewForPosition(position);
        fromScrapOrHiddenOrCache = holder != null;
    }
    // 1) Find by position from scrap/hidden list/cache
    if (holder == null) {
        //如果在上面没有找到(holder == null),那就尝试从通过 pos 在 mAttachedScrap/ mHiddenViews / mCachedViews 中获取
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
    }
    if (holder == null) {
        // 2) Find from scrap/cache via stable ids, if exists
        if (mAdapter.hasStableIds()) {
            //如果在上面没有找到(holder == null),那就尝试从通过 id 在 mAttachedScrap/ mCachedViews 中获取
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
        }
        if (holder == null && mViewCacheExtension != null) {
            //这里是通过自定义缓存中获取,忽略
        }
        //如果在上面都没有找到(holder == null),那就尝试在 RecycledViewPool 中获取
        if (holder == null) { // fallback to pool
            holder = getRecycledViewPool().getRecycledView(type);
            if (holder != null) {
                //这里拿的是,要清空数据的
                holder.resetInternal();
            }
        }
        //如果在 Scrap / Hidden / Cache / RecycledViewPool 都没有找到,那就只能创建一个了。
        if (holder == null) {
            holder = mAdapter.createViewHolder(RecyclerView.this, type);
        }
    }
    return holder;
}

做一个总结,在分析源码前,我们提出了三个问题,那看看答案是什么吧

Q:那我们看看 fill 在做什么吧?

A:其实就是分析1(回收 ViewHolder ) + 分析 2 ( 复用 ViewHolder )

Q:滑出去的 View 最后去哪里了呢?

A:先尝试回收到 mCachedViews 中,未成功,则回收到 RecycledViewPool 中。

Q:滑进来的 View 是怎么来的?

A:如果是 isPreLayout 则先从 mChangedScrap 中尝试获取。

未获取到,再从 mAttachedScrap / mHiddenViews / mCachedViews (通过 position ) 中尝试获取

未获取到,再从 mAttachedScrap / mCachedViews (通过 id)中尝试获取

未获取到,再从 自定义缓存中尝试获取

未获取到,再从 RecycledViewPool 中尝试获取

未获取到,创建一个新的 ViewHolder

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Q-CODER

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值