RecyclerView的回收复用机制

问题分析

RecyclerView是一个大的概念,从界面层次我们分析一下它的组成部分,它是由多个列表项组成的。那我们的研究对象从RecyclerView转到列表项Item上。

列表项Item有两个非常重要的内容,一个则是列表项的界面生成,这个和适配器中的onCreateViewHolder相关;另外一个则是列表项的数据绑定,这个和适配器中的onBindViewHolder相关。onCreateViewHolder是创建ViewHolder,在这个过程中我们会通过LayoutInflater去生成界面,而LayoutInflater是通过反射的方式实例化View的,基于这个情况可知创建ViewHolder是比较耗时的。onBindViewHolder是绑定相关的视图数据,这个过程中如果设置内容过多或者计算过多都会比较耗时。

如果我们RecyclerView的设计者,可以分析出onCreateViewHolder、onBindViewHolder这两个函数是比较耗时的情况下,我们会采取的策略就是减少调用,减少onCreateViewHolder和onBindViewHolder调用,达到性能最优的效果。

情况分析

如果当前情况下需要ViewHolder,又可以减少onCreateViewHolder的调用说明有同类型的缓存对象可供使用,可以不用重新创建,只需要对缓存对象重新绑定一下数据可以。

如果当前情况下需要ViewHolder,又可以减少对onBindViewHolder的调用说明有数据都不需要修改,可以直接拿过来用的缓存,这种缓存要么就是同位置的ViewHolder被回收了,要么就是同Id的ViewHolder被回收了。

缓存类型

根据上面的情况分析,我们可以得出缓存的两种类型:一种是同数据的、拿来就用、不需要做任何修改的缓存,这种类型的缓存包括Scrap、CacheViews;另外一种是同类的、拿过来之后需要重新绑定数据的缓存,这种类型的缓存是RecyclerViewPool,它给到每个Type(Type指的是适配器Adapter中的ItemType)的缓存容量默认是5。

使用场景

缓存的类型分析完后,我们就需要着重理解什么场景下需要用到缓存?那当然就是数据或者界面发生变化的时候,一类是进行滑动列表的时候,一部分Item需要离开屏幕,另外一部分Item需要进入屏幕,进入屏幕这部分Item就非常有可能是从缓存中读取的而来;另外一类就是主动的数据更新,比如通过notifyDataSetChanged更新所有Item数据、通过notifyItemChanged进行局部Item更新。

滑动

表项回收

在滑动的过程中是有item离开屏幕,有item进入屏幕,这个过程了就会涉及回收数据和读取缓存,两部分的内容,本文也是从这两个方面给大家讲解。

上图描绘的是RecyclerView的一个上滑的过程,左边用来表示一个RecyclerView的界面部分,橙色方框代表屏幕,上下两根横线分别代表着屏幕的上边界和下边界方便大家观察效果。右边是我们的缓存包括CacheViews和RecyclerViewPool两种。

当列表不断上滑的过程中了,列表项1会离开屏幕,表项如果离开屏幕就会加入到缓存,首先加入的就是CacheView缓存。

当列表项1和列表项2都加入了CacheViews并且列表项3都开始离开屏幕了,这个时候就会出现一个问题,CacheViews已经装满了,表项3又需要加入缓存,如何处理?其实很简单,最先进入的表项出去,新的进来,那么CacheViews里面装的就是表项3和表项2,表项1何处何从?它会安排进去RecyclerViewPool里面,不过这里会出现一个变化,那就是Item的数据失效了,如下图所示,所有加入RecyclerViewPool的缓存如果被读取了,都会需要重新绑定数据即会调用onBindViewHolder。

表项缓存读取

在不断向上滑动的过程中,有新的列表项进入屏幕,那新的列表项从何而来?有可能是通过onCreateViewHolder新创建的,也有可能是从缓存中读取而来。读取缓存的流程到底是如何的?我们细细探究一下。

在整个滑动的过程中,先读取的是CacheViews的缓存,那是任意一个缓存都可以?肯定不是,必须要符合条件,也就是会先匹配position(对应适配器里面的position),如果是同position缓存,说明正式需要寻找的数据。如果通过position没有寻找到数据,那就会通过id(对应适配器中getViewId中返回的id,默认是0)再寻找一遍,如果找到相同id的缓存则读取结束,没有读取到的话,就会在RecyclerViewPool中选中一个同类型(对应适配器中getViewType返回的Type)的缓存,RecyclerViewPool找到的缓存是需要重新绑定数据的。

如果RecyclerViewPool中也没有找到缓存,则会通过CreateViewHolder创建item,然后通过onBindViewHolder绑定数据,最后的这种方式就不属于读取缓存范畴,没有节省到时间。

Notify更新数据

NotifyDataSetChanged

如果调用notifyDataSetChanged这个api,那就说明主观意愿上需要重新绑定数据,表项Item的界面可以复用,但是表项展示的数据是不能用的。CacheViews这个缓存容器是用来保存不需要做任何修改的缓存,如果使用了它就不会调用onBindViewHolder绑定数据,所以不符合当前的需求,那么符合要求的只有RecyclerViewPool这个缓存容器里面来。

RecyclerViewPool对每个Type的缓存设置的大小是5,如果调用notifyDataSetChanged这个api,说明有五个Item会加入到RecyclerViewPool这一级的缓存里面来,然后在被读取重新赋予新的数据用于展示,这部分的Item是可以有效地回收复用。如果屏幕中展示的列表项Item大于五,则其他的一些列表项Item得不到及时的回收到缓存容器中,能复用但是又没利用上,而新的同位置的列表项因读取不到缓存而需要重新创建,相当于浪费了一部分的空间和性能,这种情况是非常不友好的。

如果一定要通过 notifyDataSetChange 方法更新数据,可以通过下面这种方式,在变更前调大缓存,变更完成后,调小缓存。这样布局变化也可以最大程度地复用已有的 ViewHolder。

mRecyclerView.getRecycledViewPool().setMaxRecycledViews(0, 屏幕显示的item总数+1 );mAdapter.notifyDataSetChanged();new Handler().post(new Runnable() {    @Override    public void run() {        mRecyclerView.getRecycledViewPool()                .setMaxRecycledViews(0, 5);    }});复制代码

setMaxRecycledViews中填入的第一个参数是Type,你回收复用的列表项的Item,第二个参数是数目,一般是当前屏幕显示Item的总数加一,加一的原因是因为RecyclerView除了加载当前屏幕的Item以外了,还会额外再加载一个列表项。

之所以重新又将MaxRecycledViews的值设置回5,是因为缓存空间变大是满足当前notifyDataSetChange 的需求,后面的一些业务并不是缓存空间越大越好,调回来也是为了减少对后续操作的影响。

NotifyItemChanged

如何只需要更新一个列表项的话,我们可以调用notifyItemChanged,这样的话就只是局部更新,相对来说性能消耗会小很多。如果当前屏幕内有六个列表项Item1...Item6,我们点击其中的Item4,在这个过程中了,Item4的界面可以复用,数据需要重新绑定,Item1、Item2、Item5、Item6界面和数据都可以复用不需要做任何的改变,那RecyclerView是如何处理这个操作的呢?不着急我们慢慢讲,Item1、Item2、Item5、Item6会被加入到Scrap中的mAttachedScrap里面,需要更换数据的Item4会被加入到mChangedScrap缓存中,相信大家看完很清楚,不要变的放一起,需要变化下的又放另外一个容器。之后再通过比对position、id等方式读取出缓存的内容,进行相关的测量布局设置工作,那整个过程就讲清楚了。

总结

滑动过程和CacheViews、RecyclerViewPool有关,和Scrap缓存无关。Notify更新过程与Scrap、RecyclerViewPool有关,与CacheViews无关。

Scrap中的AttachedScrap、ChangedScrap没有大小限制,CacheViews默认是2,RecyclerViewPool默认大小是5。

读取缓存都先倾向于不需要做任何改变的缓存,读取过来直接用,通过Position、ID的方式比对,如果没有命中则去RecyclerViewPool中读取,这类的缓存不论如何都需要重新绑定数据。

你可能感兴趣的:(RecyclerView的回收复用机制)