Android——RecyclerView缓存机制

发布时间 2023-04-27 01:37:05作者: 虞美人体重90

RecyclerView是一种高度可定制的View控件,它可以用于显示大量的数据集合,用一种更有效的方式来管理数据的展示和滚动。

RecyclerView之所以那么高效有很大程度上归功于它的缓存机制。

一.使用步骤:

1.添加依赖

implementation 'androidx.recyclerview:recyclerview:1.1.0' //recyclerview布局

2.layout文件中定义RecyclerView

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/recycler_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

<androidx.recyclerview.widget.RecyclerView android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent" />

<androidx.recyclerview.widget.RecyclerView android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent" />

3.配置布局管理器

RecyclerView recyclerView = findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new LinearLayoutManager(this));

4.创建adapter

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {

    private List<String> mData;

    public MyAdapter(List<String> data) {
        mData = data;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item, parent, false);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        holder.mTextView.setText(mData.get(position));
    }

    @Override
    public int getItemCount() {
        return mData.size();
    }

    public static class ViewHolder extends RecyclerView.ViewHolder {
        private TextView mTextView;

        public ViewHolder(View itemView) {
            super(itemView);
            mTextView = (TextView) itemView.findViewById(R.id.text_view);
        }
    }
}

5.为RecyclerView设置adapter

RecyclerView recyclerView = findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
MyAdapter adapter = new MyAdapter(data);
recyclerView.setAdapter(adapter);

 

二.缓存机制

RecyclerView把列表视图展示出来的过程主要分为三步:

  1.createViewHolder:创建条目对应的视图

   2.bindViewHolder:为条目视图绑定数据

  3.renderViewHolder:将绑定好数据的条目视图展现出来

其中createViewHolder耗时最长,因此recyclerView设计了一套缓存机制来提高列表视图的性能。

①一级

缓存

当列表处于屏幕内时,屏幕中显示的ViewHolder会缓存在mAttachedScrap、mChangedScrap中 :

mChangedScrap 表示数据已经改变的ViewHolder列表
mAttachedScrap 表示未与RecyclerView分离的ViewHolder列表

该级缓存主要用于缓存出现在屏幕内的item,当我们通过notifyItemRemoved(),notifyItemChanged()通知item发生变化的时候,通过mAttachedScrap缓存没有发生变化的ViewHolder,其他的则由mChangedScrap缓存,添加itemView的时候快速从里面取出,完成局部刷新。源码如下:

void scrapView(View view) {
    final ViewHolder holder = getChildViewHolderInt(view);
    if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
        || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
        if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {
            throw new IllegalArgumentException("Called scrap view with an invalid view."
                    + " Invalid views cannot be reused from scrap, they should rebound from"
                    + " recycler pool." + exceptionLabel());
        }
        holder.setScrapContainer(this, false);
        mAttachedScrap.add(holder);
       } else {
        if (mChangedScrap == null) {
            mChangedScrap = new ArrayList<ViewHolder>();
        }
        holder.setScrapContainer(this, true);
        mChangedScrap.add(holder);
    }
}

源码分析:

对于页面中显示的Item,当调用 LayoutManager 类的 onLayoutChildren() 方法对views进行布局,这时会将RecyclerView上的items全部暂存到一个 ArrayList 集合,这里的数据是没有做修改的,所以不用重新绑定 Adapter。而如果其他情况比如调用了 notifyItemChanged()notifyItemRangeChanged() 来通知数据发生了更新,数据或位置发生改变,那么该ViewHolder会被缓存到mChangedScrap中,这里存储的是发生了变化的ViewHolder,所以要重新走Adapter的绑定方法。

图解:

 图中的itemB删除掉,然后itemC,itemD依次移动上来,这里itemA和itemB前后参数没有发生变化(虽然itemB被移除了,但移除的时候它还是有效的,会被打上REMOVED标签,表示它是要删除的),所以他们两个存储到mAttachedscrap(),而itemC和itemD的位置发生了改变,所以他俩要存到mChangedScrap()中去。总结来说,删除itemB时,ABCD都会进入Scrap缓存,删除后,会从Scrap中将ACD取出,A的位置和数据都没有发生变化,CD的位置发生了变化但数据还是原封不动。

 

文章来源:https://blog.csdn.net/m0_51276753/article/details/125667231

复用

ViewHolder的复用是有顺序的,首先会判断是否预布局。

如果是就从一级缓存中的mChangedScrap()中获取。

如果没获取到就去mAttachScrap()和二级缓存中找。

而一级缓存之所以说轻量,首先是因为它只针对当前页面显示的这些item,其次是因为它用完就会清空缓存,不占空间,效率也快。

所以通知数据更新我们推荐使用notifyItemChanged(),实现局部刷新,用的是一级缓存来实现复用。而如果我们调用notifyDataChanged()来通知更新,会使数据全部进行刷新,不会走Scrap,性能低下。

②二级

缓存

当列表滑动出了屏幕时,ViewHolder会被缓存在 mCachedViews列表 ,其大小由mViewCacheMax决定,默认DEFAULT_CACHE_SIZE为2,可通过Recyclerview.setItemViewCacheSize()动态设置。

当一个viewHolder从屏幕中消失时,会把它先放进mCachedViews中,这个时候,其数据(position等)、状态均未丢失,当列表滑动,需要该position的viewHolder展示时,会先从mCachedViews中找该实例,如果找到了,那就拿出来进行展示。mCachedViews是一个缓存列表,用于移出屏幕表项的回收和复用,不会清空数据。

 复用 查找顺序:先判断是否为预布局,如果是则从一级缓存中的mChangedScrap()中获取。如果不是,则进入一级缓存的mAttachScrap()中查找,mAttachScrap()没有就进入二级缓存mCachedViews列表查找。

// Search in our first-level recycled view cache. 官方说这里是第一级,但在我们日常使用中还是称他为第二级缓存
// 查找过程
// 这是根据position来取
final int cacheSize = mCachedViews.size(); for (int i = 0; i < cacheSize; i++) { final ViewHolder holder = mCachedViews.get(i); // invalid view holders may be in cache if adapter has stable ids as they can be // retrieved via getScrapOrCachedViewForId // 这里要对索引进行判断,只有当位置对得上才能拿来复用, // 这也就意味着从mCatchedViews中取出的ViewHolder只能复用到指定的位置。 if (!holder.isInvalid() && holder.getLayoutPosition() == position && !holder.isAttachedToTransitionOverlay()) { // 如果不在容量范围内,就把ViewHolder丢出去,丢到缓存池中。 if (!dryRun) { mCachedViews.remove(i); } if (DEBUG) { Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position + ") found match in cache: " + holder); } return holder; } }

mAttachedScrapp和mCachedViews都是需要进行索引判断,也就是说从这两个缓存中取出的ViewHolder只能复用到指定的位置。mCachedViews只能缓存屏幕外它容量大小的ViewHolder,超出容量的部分会被移除,丢到缓存池中。

默认大小为2,可通过Recyclerview.setItemViewCacheSize()动态设置。

③三级缓存

可以自己实现ViewCacheExtension类实现自定义缓存,可通过Recyclerview.setViewCacheExtension()设置。

如果我们自定义了一个缓存并且前面的一二级缓存没有找到ViewHolder,系统就会从我们自定义的这个缓存里去找ViewHolder。

④四级

缓存

ViewHolder首先会缓存在 mCachedViews 中,当超过了个数(比如默认为2), 就会按照先入先出原则被移出mCachedViews列表,添加到 RecycledViewPool 中。

RecycledViewPool 会根据每个ViewType把ViewHolder分别存储在不同的列表中,一个viewType对应一个mRecyclerPool,每个ViewType最多缓存DEFAULT_MAX_SCRAP = 5 个ViewHolder。

此时其数据、状态会被重置(position置为-1,bindViewHolder中的操作均失效)。

需要展示在屏幕中的viewholder会先在mCachedViews中找position对应的viewHolder,如果未找到,那么就去对应viewType的mRecyclerPool中找该对象。

RecycledViewPool源码:

public static class RecycledViewPool {
    //同类ViewHolder缓存个数上限为5
    private static final int DEFAULT_MAX_SCRAP = 5;
    // Tracks both pooled holders, as well as create/bind timing metadata for the given type.
    // 回收池中存放单个类型ViewHolder的容器
    static class ScrapData {
        //同类ViewHolder存储在ArrayList中
        ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
        int mMaxScrap = DEFAULT_MAX_SCRAP;
    }
    //回收池中存放所有类型ViewHolder的容器
    SparseArray<ScrapData> mScrap = new SparseArray<>();
    ...
    //ViewHolder入池按viewType分类入池,一个类型的ViewType存放在一个ScrapData中
    public void putRecycledView(ViewHolder scrap) {
        final int viewType = scrap.getItemViewType();
        final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
        //如果超限了,则放弃入池
        if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
            return;
        }
        if (DEBUG && scrapHeap.contains(scrap)) {
            throw new IllegalArgumentException("this scrap item already exists");
        }
        scrap.resetInternal();
        //回收时,ViewHolder从列表尾部插入
        scrapHeap.add(scrap);
    }
    
    //从回收池中获取ViewHolder对象
    public ViewHolder getRecycledView(int viewType) {
        // 获取到viewType
        final ScrapData scrapData = mScrap.get(viewType);
        if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
            final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
            //复用时,从列表尾部获取ViewHolder(优先复用刚入池的ViewHoler)
            return scrapHeap.remove(scrapHeap.size() - 1);
        }
        return null;
    }
}

RecycledViewPool中的ViewHolder存储在SparseArray中,并且按viewType分类存储,同一类型的ViewHolder存放在一个ArrayList中。虽然没有了对索引的判断,但是从mRecyclerPool中取出的ViewHolder只能复用于相同viewType的表项。

复用

holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
// 这里是getRecyclerViewPool(),主要作用就是new了一个RecyclerViewPool对象出来。 // 然后再根据type来从缓存池中获取对应类型的ViewHolder。 RecycledViewPool getRecycledViewPool() { if (mRecyclerPool == null) { mRecyclerPool = new RecycledViewPool(); } return mRecyclerPool; }

当ViewHolder从缓存池取出来后,判断holder是否为空,如果不为空,说明holder从缓存池中取出来了。那么就执行 holder.resetInternal(),该方法是将数据,状态重置。

把该对象拿出来,重新走一遍bindViewHolder、renderViewHolder,进行展示。

如果holder为空,则未找到,那么就走create、bind、renderViewHolder的完整流程。