先说结论吧
RecycleView有4级缓存
1.Attached scrap & Changed scrap
屏幕中的缓存,用于数据刷新时,不需要重新加载子ItemView,直接复用
2.mCachedViews
刚刚移出屏幕的缓存,最大容量为2(可以考虑自行修改),通过position来保存,数据不变,直接复用;滑动时,该缓存一边add,一边remove.后面会解释
3.mViewCacheExtension自定义缓存,没用过
4.mRecyclerPool保存mCachedViews缓存中保存不了的ItemView。通过itemType来保存,每种itemType可以保存5个ItemView,可以通过RecycleView#recycledViewPool.setMaxRecycledViews(int max)来修改,但是会增加内存开销,如果当前缓存没有找到对应itemType的ViewHolder,那么会重新走onCreateViewHolder以及onBindViewHolder。
以上如果有命中缓存,只会执行onBindViewHolder
RecycleView滑动过程一定能命中缓存吗?
不一定,我们知道,命中缓存的关键,主要依靠mCachedViews(默认2个)跟mRecyclerPool(默认5个)。如果说,当前的数据viewType是按顺序,依据类型添加的。那么肯定可以进行缓存复用。代码如下:
for(i in 0 until 20){
list.add(MyVhDataList(1, "holderA$i"))
}
for (i in 0 until 20){
list.add(MyVhDataList(2, "holderB$i"))
}
从list数据结构可以看出,我们一次添加了viewType为1的数据20个,viewType为2的数据20个。根据缓存的规则,我们任意上下滑动,都会对当前的viewType对应的mRecyclerPool进行赋值(最大长度为5),并从mRecyclerPool取出对应viewType所对应的holder对象,进行复用。简单说就是不会从小走onCreateViewHolder,只会执行onBindViewHolder
但是!!!如果数据类型如下:
for(i in 0 until 20){
list.add(MyVhDataList(1, "holderA$i"))
}
for (i in 0 until 20){
list.add(MyVhDataList(2, "holderB$i"))
}
for (i in 0 until 20){
list.add(MyVhDataList(3, "holderC$i"))
}
for(i in 0 until 20){
list.add(MyVhDataList(1, "holderA$i"))
}
从数据初始化,我们可以看出,我们依次添加了viewType为1的holder20个,viewType为2的holder20个,viewType为3的holder20个,最后,又添加viewType为1的holder20个。继续根据回收复用原理,来理解。
1.假设,我首次渲染数据,一屏可以显示6个数据,加上预加载的2个数据(这里暂时不分析预加载)。那么我会执行8次onCreateViewHolder。当我持续往上滑动的时候,又会执行3次onCreateViewHolder,总共执行11次onCreateViewHolder。后续就是直接复用已创建好的这些ViewHolder。
源码Recycleview#tryGetViewHolderForPositionByDeadline从这里来分析,获取复用数据
注意!!!!这里首先是上滑!上滑!上滑!上滑!上滑!
1.把屏幕前2个item滑出当前屏幕区域会直接放入mCachedViews,
2.把第3个item滑动屏幕,会把当前viewHolder添加(add)到mCachedViews,然后判断mCachedViews.size,
如果超过2个,就remove第一个,并准备放入mRecyclerPool,进而判断mRecyclerPool内部对应viewType的scrapHeap.size,大于5直接返回,否则,放出mRecyclerPool。
3.后续,每个viewType为1的item滑出屏幕后,都会执行第二步操作。
这里着重记住一点。mCachedViews的复用,只会判断position,所以,我们这里只对pool进行分析。考虑到每次item滑出屏幕,就一定有item滑进屏幕。所以复用的顺序是,先addViewHolderToRecycledViewPool,再执行getRecycledView#scrapHeap.remove(i)
4.也就是说,当我们把前面2个item滑出屏幕的时候,都会去找能否有复用的holder,
找不到就会执行onCreateViewHolder。这里执行了2次。
到目前为止,总共执行了10次onCreateViewHolder。
这个时候mCachedViews,保存了两个holder,mRecyclerPool保存了0个holder。
当我们把第三个item滑出屏幕的时候,会先去mCachedViews找能否复用的holder,这个时候是找不到的。
所以又去mRecyclerPool找,这个时候也是找不到的。
于是执行了第11次onCreateViewHolder。
执行第二步,最后,mCachedViews,总共有2个holder。
mRecyclerPool有一个holder。
5.当我们把第4个item滑出屏幕的时候,执行第二步,先把holder放入mCachedViews,由于mCachedViews.size>2。所以,会把mCachedViews[0]。放入mRecyclerPool。此时mRecyclerPool里面有两个holder,由于在mCachedViews找不到对应的holder复用。
于是,继续去mRecyclerPool找,这个时候,找到对应viewType存放的1个holder,取出进行复用。并删除当前mRecyclerPool存放的viewType。
6.后续,就是重复以上第5步骤
4.综上:有2个item对应的ViewHolder放入mCachedViews,有2个放入mRecyclerPool,根据我们上面说的,
总共会执行11次onCreateViewHolder,
以上分析的是上滑,对mCachedViews以及mRecyclerPool进行赋值
以下分析的是下滑!下滑!!下滑!!下滑!!
下滑其实跟上面讲的第5个步骤一样。。。。。
这里说重点了,为毛上述的数据结构,滑动的时候还会继续执行onCreateViewHolder,也就是为毛找不到可以复用的holder。
1.根据上述分析,当我们把第一个type==1的holder全部划走之后,由于底下是type==2、3的holder。所以mRecyclerPool会一直存放viewType为1的holder,并不会复用取出,mRecyclerPool对应viewType==1最多有5个
当我们继续滑动其他类型的holder的时候,mRecyclerPool存放的是viewType为2、3的holder,当我们继续上滑到另外一组viewType为1的时候,由于mRecyclerPool存放的viewType为1的holder有5个,当取出复用后,并没有viewType为1的holder继续存放到mRecyclerPool。所以,当我们滑动的过程中,只有一部分viewType为1的holder能被找到并复用。直到,屏幕中全部都是viewType为1的holder后,重新执行回收复用流程,才能继续复用。
结论
以上就是整个Recycleview复用holder的流程。有点啰嗦。尽量不讲源码,用案例来解释。总之一定注意,如果为了复用的可行性以及高效性。尽量一定要显示ABC这种数据类型。如果有ABCA这种情况。一部分item的复用就失效了。
这里补充一句,有些业务都是使用ScrollView嵌套Recycleview的形式。这种也会让复用失效。
解决方案,参考
解决NestedScrollView与RecyclerView嵌套,导致RecyclerView无法复用,滑动冲突等问题
另外还有一种业务涉及到垂直Recycleview嵌套item水平Recycleview。这种情况水平的Recycleview的复用也会失效,暂时没有去研究。
优化方案,参考,直接看第六点吧。
共享RecycledViewPool
说实话,RecycleviewView的设计简直牛逼。但是,使用不规范,确实很难排查并优化了。