RecyclerView(二) — 缓存机制分析

一、概述

我们知道 RecyclerView 其实是继承 ViewGroup 实现的,也就是说它的最根本的功能是可以往 RecyclerView 中添加子 View。如往 LinearLayout 中添加子 View,子 View 就会横向或纵向展开排列。以 LinearLayout 纵向排列为例,当子 View 过多时,超出屏幕部分的子 View 就显示不出来,这时引入一种 滑动机制 来确保所有子 View 都可以展示 (通过 Scroller/ OverScroller 类辅助实现滑动效果),类似 ScrollerView。当子 View 数量很多时,又会产生另一个新问题 (内存占用大:因为所有子 View 需要同时存在)。但是这种场景下有个特点,即屏幕展示的子 View 有限,大部分子 View 都不可见。所以新增了 View 的复用机制,即通过回收不可见的子 View 并在展示新 View 时进行复用来实现优化。这便是 ListView、RecyclerView 等存在的意义。

版本: Android SDK 29

关联文章:

  1. 《RecyclerView(一) — 绘制流程分析》

RecyclerView 的关联模块:

  • LayoutManager:负责 RecyclerView 中,控制 item 的布局方向
  • Recycler:负责View的缓存。
  • RecyclerView.Adapter:为 RecyclerView 承载数据
  • ItemDecoration:为 RecyclerView 添加分割线
  • ItemAnimator:控制 RecyclerView 中 item 的动画

前面我们刚刚分析了RecyclerView 三大绘制流程,接下来我们重点分析缓存原理。


二、缓存的分类

Recycler 负责管理废弃或被 detached 的 item 视图,以便重复利用。先来看下他的四级缓存:

缓存级别缓存作用
一级缓存mAttachedScrap、mChangedScrap不参与滑动时的回收复用,仅作为重新布局时的一种临时缓存。
二级缓存mCachedViews是一个 ViewHolder 集合,负责对刚刚移出屏幕的View进行回收复用的缓存列表。
三级缓存ViewCacheExtension用户自定义的缓存 (一般不使用)
四级缓存RecycledViewPoolViewHolder 缓存池,可以实现多个RecyclerView公用。以viewType为key可以缓存多个同类型的ViewHolder。

2.1 Scrap 缓存

Scrap 是 RecyclerView 中最轻量的缓存,它不参与滑动时的回收复用,仅作为重新布局时的一种临时缓存。目的是缓存当界面重新布局的前后都出现在屏幕上的ViewHolder,以此省去不必要的重新加载与绑定工作。

  • mAttachedScrap:负责保存将会原封不动的ViewHolder。
  • mChangedScrap:负责保存位置会发生移动的ViewHolder,注意只是位置发生移动,内容仍原封不动。
public final class Recycler {
	// 负责保存将会原封不动的ViewHolder。
    final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
    // 负责保存位置会发生移动的ViewHolder,注意只是位置发生移动,内容仍原封不动。
    ArrayList<ViewHolder> mChangedScrap = null;
}

上面两个 Scrap 缓存的实际应用场景如下图所示:(图片来源 RecyclerView源码解析)
在这里插入图片描述

2.2 CachedViews 缓存

CacheView 是以 ViewHolder 为单位,负责在 RecyclerView 列表位置产生变动的时候,对刚刚移出屏幕的 View 进行回收复用的缓存列表。

public final class Recycler {
	// 缓存刚刚移除屏幕的ViewHolder。
	final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();

    private int mRequestedCacheMax = DEFAULT_CACHE_SIZE; //DEFAULT_CACHE_SIZE=2
    int mViewCacheMax = DEFAULT_CACHE_SIZE;
}

上面 CachedViews 缓存的实际应用场景如下图所示:(图片来源 RecyclerView源码解析
在这里插入图片描述

2.3 ViewCacheExtension 缓存

ViewCacheExtension 是一个由用户自定义的缓存,其定义如下:

public abstract static class ViewCacheExtension {

    public abstract View getViewForPositionAndType(
    				@NonNull Recycler recycler, int position, int type);
}

小结:

  • RecyclerView 调用缓存的顺序, Scrap缓存 -> CacheViews 缓存 -> ViewCacheExtension 缓存 -> RecyclerViewPool 缓存。
  • ViewCacheExtension 缓存只有取缓存的接口,但是 Recycler 没有对应添加缓存的接口,因此需要自己找场景插入添加缓存的逻辑。

2.4 RecycledViewPool 缓存

在 Srap 和 CacheView 不愿意缓存的时候,都会放入 RecycledViewPool 进行回收。同时,RecycledViewPool 只按照 ViewType 进行区分,只要 ViewType 是相同的,甚至可以在多个 RecyclerView 中进行通用的复用,只要为它们设置同一个RecycledViewPool 就可以了。

public static class RecycledViewPool {
	// 默认最多缓存5个。
    private static final int DEFAULT_MAX_SCRAP = 5;
    SparseArray<ScrapData> mScrap = new SparseArray<>();

    static class ScrapData {
        final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
        int mMaxScrap = DEFAULT_MAX_SCRAP;
        long mCreateRunningAverageNs = 0;
        long mBindRunningAverageNs = 0;
    }
}

RecycledViewPool 的数据结构如下图所示,可以缓存 viewType 相同的多个 ViewHolder。
在这里插入图片描述


三、缓存的操作 (存/取)

上面介绍完了缓存的分类及其特点之后,下面我们来看看这个类型的缓存在哪些地方存取的。

3.1 缓存的回收

Condition1:Scrap 回收

《RecyclerView(一) — 绘制流程分析》 中我们知道,在调用LayoutManager.onLayoutChildren() 方法时触发了 Scrap 的缓存。

下面我们就来分析一下这个缓存的流程。

// LayoutManager.class
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
	detachAndScrapAttachedViews(recycler);
}

public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
  	final int childCount = getChildCount();
    for (int i = childCount - 1; i >= 0; i--) {
        final View v = getChildAt(i);
        scrapOrRecycleView(recycler, i, v);
    }
}

private void scrapOrRecycleView(Recycler recycler, int index, View view) {
	// 获取每个Item对应的ViewHolder。
    final ViewHolder viewHolder = getChildViewHolderInt(view);
    if (viewHolder.shouldIgnore()) {
        return;
    }
    if (viewHolder.isInvalid() && !viewHolder.isRemoved()
            && !mRecyclerView.mAdapter.hasStableIds()) {
        removeViewAt(index);
        recycler.recycleViewHolderInternal(viewHolder);
    } else {
        detachViewAt(index);
        // 最终在这里会缓存到对应的Scrap缓存中。
        recycler.scrapView(view);
        mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
    }
}

Condition2:滑动中的 ViewHolder 回收

下面介绍在滑动中回收 ViewHolder 的流程。

// LayoutManager.class

// 调用链:
//fill()
//->recycleByLayoutState()
//->recycleViewsFromStart()
//->recycleChildren()
//->LayoutManager.removeAndRecycleViewAt()

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
        RecyclerView.State state, boolean stopOnFocusable) {
	// View 的回收逻辑
    recycleByLayoutState(recycler, layoutState);
}

private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
    if (!layoutState.mRecycle || layoutState.mInfinite) {
        return;
    }
    int scrollingOffset = layoutState.mScrollingOffset;
    int noRecycleSpace = layoutState.mNoRecycleSpace;
    if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
    	// 从End端开始回收视图
        recycleViewsFromEnd(recycler, scrollingOffset, noRecycleSpace);
    } else {
    	// 从Start端开始回收视图
    	// 调用链:recycleViewsFromStart()->recycleChildren()->LayoutManager.removeAndRecycleViewAt()
        recycleViewsFromStart(recycler, scrollingOffset, noRecycleSpace);
    }
}

public void removeAndRecycleViewAt(int index, @NonNull Recycler recycler) {
    final View view = getChildAt(index);
    removeViewAt(index);
    recycler.recycleView(view);
}

RecycleView.recycleView()

// RecycleView.class
public void recycleView(@NonNull View view) {
    ViewHolder holder = getChildViewHolderInt(view);
    //...清除状态...
    
    // 1.回收ViewHolder
    recycleViewHolderInternal(holder);
    
    //...
}


void recycleViewHolderInternal(ViewHolder holder) {
    //...各种无法回收逻辑的判断...
    
    boolean cached = false;
    boolean recycled = false;

    if (forceRecycle || holder.isRecyclable()) {
        if (mViewCacheMax > 0
                && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                | ViewHolder.FLAG_REMOVED
                | ViewHolder.FLAG_UPDATE
                | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
            // Retire oldest cached view
            // 滑动的视图,先保存在mCachedViews中
            int cachedViewSize = mCachedViews.size();
            if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
            	// mCachedViews只能缓存mViewCacheMax个,如果保存不下,
            	// 就将mCachedViews中最久的那个移到RecycledViewPool。
                recycleCachedViewAt(0);
                cachedViewSize--;
            }

            int targetCacheIndex = cachedViewSize;
            if (ALLOW_THREAD_GAP_WORK
                    && cachedViewSize > 0
                    && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
                // when adding the view, skip past most recently prefetched views
                int cacheIndex = cachedViewSize - 1;
                while (cacheIndex >= 0) {
                    int cachedPos = mCachedViews.get(cacheIndex).mPosition;
                    if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
                        break;
                    }
                    cacheIndex--;
                }
                targetCacheIndex = cacheIndex + 1;
            }
            // 将本次回收的ViewHolder放到mCachedViews中.
            mCachedViews.add(targetCacheIndex, holder);
            cached = true;
        }
        if (!cached) {
        	// 如果没能缓存到CacheViews里,则缓存到RecycledViewPool。
            addViewHolderToRecycledViewPool(holder, true);
            recycled = true;
        }
    } else {
        //
    }
    // even if the holder is not removed, we still call this method so that it is removed
    // from view holder lists.
    mViewInfoStore.removeViewHolder(holder);
    if (!cached && !recycled && transientStatePreventsRecycling) {
        holder.mOwnerRecyclerView = null;
    }
}

小结:

滑动时的View回收的过程:

  1. 判断是否满足回收条件,如果不满足,则直接抛出异常。
  2. 满足回收条件 & 满足缓存到 CachedViews 的条件都成立。
    • 如果CachedViews 条数未达到最大值,则直接缓存 ViewHolder。
    • 如果CachedViews 条数已经达到最大值,则将 CachedViews 中最老的一个 ViewHolder 移到 RecycledViewPool 中,然后缓存ViewHolder。
  3. 如果 CachedViews 没有缓存成功,则直接将 ViewHolder 缓存到 RecycledViewPool 。当 RecycledViewPool 中ViewType 对应的ViewHolder缓存数量达到最大值时,则不再缓存。

3.2 缓存的获取

// LayoutManager.class
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
	View view = layoutState.next(recycler);
	//...
}

// LayoutState.class
View next(RecyclerView.Recycler recycler) {
    if (mScrapList != null) {
        return nextViewFromScrapList();
    }
    // 最终调用 Recycler.tryGetViewHolderForPositionByDeadline()方法
    final View view = recycler.getViewForPosition(mCurrentPosition);
    mCurrentPosition += mItemDirection;
    return view;
}

// Recycler.class
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
   		boolean dryRun, long deadlineNs) {
	// ...
    boolean fromScrapOrHiddenOrCache = false;
    ViewHolder holder = null;
    // 0) If there is a changed scrap, try to find from there
    // 1.先从mChangedScrap中获取。当数据位置发生变化的时候,会走这个逻辑。
    // 例如:notifyItemRemove()后,下面的数据会上移,会走这个逻辑。
    if (mState.isPreLayout()) {
        holder = getChangedScrapViewForPosition(position);
        fromScrapOrHiddenOrCache = holder != null;
    }
    // 1) Find by position from scrap/hidden list/cache
    if (holder == null) {
    	// 2.根据position依次从mAttachedScrap、隐藏的列表、mCachedViews中获取。
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
        if (holder != null) {
        	// 检验获取的holder是否合法。
        	// 不合法:就会将holder进行回收。
        	// 合法:则标记fromScrapOrHiddenOrCache为true。表明holder是从这缓存中获取的。
            if (!validateViewHolderForOffsetPosition(holder)) {
                if (!dryRun) {
                    holder.addFlags(ViewHolder.FLAG_INVALID);
                    if (holder.isScrap()) {
                        removeDetachedView(holder.itemView, false);
                        holder.unScrap();
                    } else if (holder.wasReturnedFromScrap()) {
                        holder.clearReturnedFromScrapFlag();
                    }
                    recycleViewHolderInternal(holder);
                }
                holder = null;
            } else {
                fromScrapOrHiddenOrCache = true;
            }
        }
    }
    
    if (holder == null) {
        //...
        // 2) Find from scrap/cache via stable ids, if exists
        if (mAdapter.hasStableIds()) {
        	// 3.根据id依次尝试从 mAttachedScrap、mCachedViews中获取。
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
            if (holder != null) {
                // update position
                holder.mPosition = offsetPosition;
                fromScrapOrHiddenOrCache = true;
            }
        }
        if (holder == null && mViewCacheExtension != null) {
            // We are NOT sending the offsetPosition because LayoutManager does not
            // know it.
            // 4.尝试从我们自定义的 mViewCacheExtension中去获取。
            final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
            if (view != null) {
                holder = getChildViewHolder(view);
                if (holder == null || holder.shouldIgnore()) {
                    throw new IllegalArgumentException("msg");
                }
            }
        }
        if (holder == null) { // fallback to pool
         	// 5.从RecycledViewPool缓存池里面获取。
            holder = getRecycledViewPool().getRecycledView(type);
            if (holder != null) {
                holder.resetInternal();
                if (FORCE_INVALIDATE_DISPLAY_LIST) {
                    invalidateDisplayListInt(holder);
                }
            }
        }
        if (holder == null) {
            long start = getNanoTime();
            if (deadlineNs != FOREVER_NS
                    && !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
                // abort - we have a deadline we can't meet
                return null;
            }
            // 6.缓存中找不到,就创建新的ViewHolder。
            holder = mAdapter.createViewHolder(RecyclerView.this, type);
            if (ALLOW_THREAD_GAP_WORK) {
                // only bother finding nested RV if prefetching
                RecyclerView innerView = findNestedRecyclerView(holder.itemView);
                if (innerView != null) {
                    holder.mNestedRecyclerView = new WeakReference<>(innerView);
                }
            }

            long end = getNanoTime();
            mRecyclerPool.factorInCreateTime(type, end - start);
        }
    }
    //...
    
    boolean bound = false;
    if (mState.isPreLayout() && holder.isBound()) {
        // do not update unless we absolutely have to.
        // 数据不需要绑定(一般从mChangedScrap,mAttachedScrap中得到的缓存Holder是不需要进行重新绑定的)
        holder.mPreLayoutPosition = position;
    } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        // 调用Adapter.bingdViewHolder()
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }
    //...
    return holder;
}

小结:

获取缓存的流程:

  1. 从 mChangedScrap 中获取去获取。
  2. 根据 position 依次尝试从 mAttachedScrap、隐藏的列表、mCachedViews 中获取。
  3. 根据 id 依次尝试从 mAttachedScrap、mCachedViews 中获取。
  4. 尝试从我们自定义的 mViewCacheExtension 中去获取。
  5. 根据 ViewType 从缓存池里面获取。
  6. 如果上面都无法获取的话就通过 Adapter.createViewHolder() 来创建ViewHolder。

四、RecyclerView 与 ListView 的区别

ListView:2级缓存
在这里插入图片描述

RecyclerView:4级缓存
在这里插入图片描述

ListView 和 RecyclerView 缓存机制基本一致:

  • mActiveViews 和 mAttachedScrap 功能相似,目的在于快速重用屏幕上可见的列表项 ItemView,而不需要重新 createView 和 bindView。
  • mScrapView 和 mCachedViews + mReyclerViewPool 功能相似,目的在于缓存离开屏幕的 ItemView,目的是让即将进入屏幕的ItemView重用。
  • RecyclerView 的优势在于 mCacheViews 的使用,可以做到屏幕外的列表项 ItemView 进入屏幕内时也无须 bindView,可以快速重用。同时,RecyclerViewPool 可以供多个RecyclerView共同使用。

缓存层级不同:

  • ListView 2级缓存,RecyclerView 4级缓存,且RecyclerViewPool 支持多个 RecyclerView 共享。

缓存形式不同:

  • RecyclerView 缓存的是 ViewHolder (即 View + ViewHolder +Flag 标识状态),避免每次 createView 时调用findViewById。
  • ListView缓存View,所以每次从缓存获取时需要重新 bindView。

局部刷新:

  • 在ListView 中刷新数据通常使用 notifyDataSetChanged() 进行全局刷新,非常消耗资源。
  • RecyclerView 中可以实现局部刷新。ListView 的局部刷新效果需要自己实现。

动画效果:

  • 在RecyclerView 中支持动画效果的接口,我们只需要实现自定义的动画效果即可。
  • ListView 需要手动在 Adapter 中实现动画效果。

五、参考

RecyclerView卡顿的问题可能有多种原因。其中一种可能是由于频繁创建和销毁ViewHolder导致的。虽然使用ViewHolder可以复用布局,但是如果RecyclerView中的数据量很大,每次滚动时都需要创建和销毁大量的ViewHolder,就会导致卡顿。 为了解决这个问题,我们可以考虑使用RecyclerView缓存机制,通过调整缓存的大小来减少ViewHolder的创建和销毁次数。 另外,嵌套RecyclerView也可能引起卡顿问题。尽管在实际使用中经常会看到NestedScrollView嵌套RecyclerView的做法,但是谷歌官方并不推荐这样做。因为每一条数据都会创建一个item,当数据量很大时,会不断发生垃圾回收(GC),进而影响UI线程的流畅度。 如果遇到这样的情况,我们可以考虑使用其他布局方式来代替嵌套RecyclerView,或者尝试对数据进行分页加载。 除了以上两个原因外,还有其他可能导致RecyclerView卡顿的情况,比如列表项布局复杂、数据处理耗时等。解决这些问题的方法也各有不同,可以根据具体情况进行优化。例如,可以尽量减少列表项布局的复杂度,避免使用过多的嵌套布局;对于数据处理耗时的情况,可以考虑使用异步操作或者分批处理数据。 综上所述,RecyclerView卡顿问题的解决方法包括优化ViewHolder的创建和销毁、避免嵌套RecyclerView以及针对具体情况进行其他优化措施。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [RecyclerView的卡顿分析与解决方案](https://blog.csdn.net/weixin_37228152/article/details/107525919)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [RecyclerView的卡顿优化(一)](https://blog.csdn.net/likuan0214/article/details/51899400)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值