RecyclerView缓存和复用机制详解

引入依赖库:implementation 'androidx.recyclerview:recyclerview:1.1.0'

一、RecyclerView的介绍

从名字可以看出Recycler是可回收复用的意思,recyclerView就是可回收缓存并复用的View,回收缓存的是itemView,复用的是itemView(itemView就是你为每种type写的R.layout.xxx布局,LayoutInflater.inflate()的那个view),RecyclerView本身就是继承ViewGroup的,是一个可以对其内部的子控件进行回收复用的容器。

RecyclerView这个系统依赖库提供的类,内部代码有13000多行,其内部提供了好多帮助辅助内部类:RecyclerView.Adapter适配器、RecyclerView.ViewHolder布局包装类、RecyclerView.ItemDecoration分割线、RecyclerView.LayoutManager布局管理器、RecyclerView.Recycler回收类等等,前4个是我们使用频率超高的。

RecyclerView这个控件几乎所有的Android开发者都使用过(甚至不用加几乎),它是真的很好用,完美取代了ListView和GridView,而RecyclerView之所以好用,得益于它优秀的缓存机制。关于RecyclerView缓存机制,更是需要我们开发者来掌握的。本文就将先从整体流程看RecyclerView的缓存,再带你从源码角度分析,跳过读源码的坑,最后用一个简单的demo的形式展示出来。在开始RecyclerView的缓存机制之前我们先学习关于ViewHolder的知识。

二、ViewHolder

1、为什么单独介绍ViewHolder,这个类在RecyclerView中充当什么角色?

使用RecyclerView必须要使用ViewHolder这个类,ViewHolder是itemView的包装类,对构造方法传进去的itemView进行了一层包装,理论上可以说ViewHolder就是itemView,一个itemView对应一个ViewHolder,一对一的关系,而我们后面多级缓存复用的就是这个ViewHolder

2、RecyclerView为什么强制我们实现ViewHolder模式?

关于这个问题,我们首先看一下ListView。ListView是不强制我们实现ViewHolder的,但是后来google建议我们实现ViewHolder模式。我们先分别看一下这两种不同的方式。

其实这里我已经用红框标出来了,ListView使用ViewHolder的好处就在于可以避免每次getView都进行findViewById()操作,因为findViewById()利用的是DFS算法(深度优化搜索),是非常耗性能的。而对于RecyclerView来说,强制实现ViewHolder的其中一个原因就是避免多次进行findViewById()的处理,另一个原因就是因为ItemView和ViewHolder的关系是一对一,也就是说一个ViewHolder对应一个ItemView。这个ViewHolder当中持有对应的ItemView的所有信息,比如说:position;view;width等等,拿到了ViewHolder基本就拿到了ItemView的所有信息,而ViewHolder使用起来相比itemView更加方便。RecyclerView缓存机制缓存的就是ViewHolder(ListView缓存的是ItemView),这也是为什么RecyclerView为什么强制我们实现ViewHolder的原因。

扫描二维码关注公众号,回复: 12731655 查看本文章

三、ListView的缓存机制

ListView的缓存有两级,在ListView里面有一个内部类 RecycleBin,RecycleBin有两个对象Active View和Scrap View来管理缓存,Active View是第一级,Scrap View是第二级。

  • Active View:是缓存在屏幕内的ItemView,当列表数据发生变化时,屏幕内的数据可以直接拿来复用,无须进行数据绑定。

  • Scrap view:缓存屏幕外的ItemView,这里所有的缓存的数据都是'脏的',也就是数据需要重新绑定,也就是说屏幕外的所有数据在进入屏幕的时候都要走一遍getView()方法。

ListView的缓存机制:先去mActiveViews缓存集合中找,知道返回itemView,没找到去下一级缓存mScrapViews中找,找到返回itemView,没找到就去创建CreateView。ListView的缓存机制相对比较好理解,它只有两级缓存,一级缓存Active View是负责屏幕内的ItemView快速复用,而Scrap View是缓存屏幕外的数据,当该数据从屏幕外滑动到屏幕内的时候需要走一遍getView()方法。

ListView的源码:

四、RecyclerView的缓存机制

RecyclerView的缓存分为四级

  • Scrap

  • Cache

  • ViewCacheExtension

  • RecycledViewPool

Scrap对应ListView 的Active View,就是屏幕内的缓存数据,就是相当于换了个名字,可以直接拿来复用。

Cache 刚刚移出屏幕的缓存数据,默认大小是2个,当其容量被充满同时又有新的数据添加的时候,会根据FIFO(先进先出)原则,把优先进入的缓存数据移出并放到下一级缓存中,然后再把新的数据添加进来。Cache里面的数据是干净的,也就是携带了原来的ViewHolder的所有数据信息,数据可以直接来拿来复用。需要注意的是,cache是根据position来寻找数据的,这个postion是根据第一个或者最后一个可见的item的position以及用户操作行为(上拉还是下拉)。
举个栗子:当前屏幕内第一个可见的item的position是1,用户进行了一个下拉操作,那么当前预测的position就相当于(1-1=0),也就是position=0的那个item要被拉回到屏幕,此时RecyclerView就从Cache里面找position=0的数据,如果找到了就直接拿来复用。

ViewCacheExtension是google留给开发者自己来自定义缓存的,这个ViewCacheExtension我个人建议还是要慎用,因为我扒拉扒拉网上其他的博客,没有找到对应的使用场景,而且这个类的api设计的也有些奇怪,只有一个public abstract View getViewForPositionAndType(@NonNull Recycler recycler, int position, int type);让开发者重写通过position和type拿到ViewHolder的方法,却没有提供如何产生ViewHolder或者管理ViewHolder的方法,给人一种只出不进的赶脚,还是那句话慎用。

RecycledViewPool刚才说了Cache默认的缓存数量是2个,当Cache缓存满了以后会根据FIFO(先进先出)的规则把Cache先缓存进去的ViewHolder移出并缓存到RecycledViewPool中,RecycledViewPool默认的缓存数量是5个。RecycledViewPool与Cache相比不同的是,从Cache里面移出的ViewHolder再存入RecycledViewPool之前ViewHolder的数据会被全部重置,相当于一个新的ViewHolder,而且Cache是根据position来获取ViewHolder,而RecycledViewPool是根据itemType获取的,如果没有重写getItemType()方法,itemType就是默认的。因为RecycledViewPool缓存的ViewHolder是全新的,所以取出来的时候需要走onBindViewHolder()方法


RecyclerView的复用整体流程:首先会去Scrap这个集合mAttachedScrap中找是否有ViewHolder,有就复用ViewHolder,没有就去下一级缓存mCache这个集合mCachedViews中查找,找到就直接复用,没有就去下一级缓存自定义的mViewCacheExtension这个类中查找,但是基本很少用,没找到就去下一级RecyclerViewPool中查找,找到就直接复用,没找到就会调用我们在adapter中重写的方法onCreateViewHolder新建。

五、从源码分析RecyclerView的缓存机制

先看两个关键类:Recycler和RecyclerViewPool部分代码:

mAttachedScrap、mCachedViews和RecycledViewPool里面的mScrapHeap都是ArrayList,缓存被加入到这三个对象里面实际上就是调用的ArrayList.add()方法,复用缓存呢,这里要注意一下不是调用的ArrayList.get()而是ArrayList.remove(),其实这里也很好理解,因为当缓存数据被取出来展示到了屏幕内,自然就应该被移除。

 

接下来一步一步跟进去源码查看:首先我们要分析RecyclerView的复用机制,就得知道入口在哪里?仔细想想,什么情况下会复用?是不是上下滑动的时候,这下就清楚了,找滑动的入口。

这里我把这块查找源码的步骤列出,入口:onTouchEvent的滑动ACTIVE_ Move 事件 --> scrollByInternal --> scrollStep --> LayoutManger的子类的mLayout.scrollVerticallyBy --> scrollBy  --> fill --> layoutChunk  --> layoutState.next --> addView(view);

上面入口到layoutState.next就拿到了我们最终想要的ItemView,接着从这个方法继续跟进去:layoutState.next --> getViewForPosition --> tryGetViewHolderForPositionByDeadline -->

到了这个方法tryGetViewHolderForPositionByDeadline才是最关键的,方法返回值为ViewHolder,里面就有RecyclerView的四级缓存拿取ViewHolder和onCreateViewHolder新建ViewHolder。

这里就开始拿第一级和第二级缓存了getScrapOrHiddenOrCachedHolderForPosition()这个方法可以深入去看以下,注意这里传的参数是position(dryRun这个参数不用管),就跟我之前说的,Scrap和Cache是根据position拿到缓存。

这里开始拿第三级缓存了,这里我们不自定义ViewCacheExtension就不会进入判断条件,还是那句话慎用。

这里到了第四级缓存RecycledViewPool,getRecycledViewPool().getRecycledView(type);通过type拿到ViewHolder,接着holder.resetInternal();重置ViewHolder,让其变成一个全新的ViewHolder。

到这里如果ViewHolder还为null的话,就会create ViewHolder了,创建一个新的ViewHolder。

六、复用小结:

ListView有两级缓存,分别是Active View和Scrap View,缓存的对象是ItemView;而RecyclerView有四级缓存,分别是Scrap、Cache、ViewCacheExtension和RecycledViewPool,缓存的对象是ViewHolder。Scrap和Cache分别是通过position去找ViewHolder可以直接复用;ViewCacheExtension自定义缓存,目前来说应用场景比较少却需慎用;RecycledViewPool通过type来获取ViewHolder,每种type的itemView默认最多保存5个,获取的ViewHolder是个全新,需要重新绑定数据。当你看到这里的时候,面试官再问RecyclerView的性能比ListView优化在哪里,我想你已经有答案。

七、缓存的源码分析(缓存到scrap、cache、viewCacheExtension、RecyclerViewPool中)

上面分析了itemView的复用流程及源码,那么我们的itemView到底啥时候缓存起来呢?源码分析:

刷新布局的时候就会进行回收入口LinearLayoutManager.onLayoutChildren --> detachAndScrapAttachedViews --> scrapOrRecycleView

--> 1、recycler.recycleViewHolderInternal(viewHolder); -- 只会处理 CacheView 、RecyclerViewPool 的缓存

​    --> 1.ViewHodler改变 不会进来 -- 先判断mCachedViews的大小

​        --> mCachedViews.size 大于默认大小2  ---》就会调用 recycleCachedViewAt  --- >调用addViewHolderToRecycledViewPool --- 缓存池里面的数据都是从mCachedViews里面出来的,并从mCacheView集合中移除。

​    --> 2.条件判断的另一种情况直接保存到缓存池addViewHolderToRecycledViewPool --> getRecycledViewPool().putRecycledView(holder);缓存池和viewtype挂钩的。

​        --> scrap.resetInternal();  ViewHolder 清空数据,缓存池里面保存的ViewHolder没有数据的,当池中满了就会丢掉。这就和mcacheView集合中的不同,mcache中的是绑定了数据的,所以作用不太一样,也是缓存的一种优化。

--> 2、recycler.scrapView(view);这种缓存的就是处理scrap的情况。

上面是move的入口会进行复用的操作逻辑,这是上面复用的另一个入口:RecyclerView.onLayout --> dispatchLayout --》 dispatchLayoutStep2 --》 onLayoutChildren --》 fill--》后面的就和上面一样了。

缓存:fill里面也有缓存,不仅只有复用 -->recycleByLayoutState-->recycleViewsFromStart、recycleViewsFromEnd --> recycleChildren--> removeAndRecycleViewAt --> recycler.recycleView --> recycler.recycleViewHolderInternal(viewHolder); --这个fill入口只会处理 CacheView 、RecyclerViewPool 的缓存。

上面就介绍了两种缓存入口和两种复用入口,从源码分析了RecyclerView的整体如何复用和如何缓存。

 

猜你喜欢

转载自blog.csdn.net/sunbinkang/article/details/114401534