欢迎光临散文网 会员登陆 & 注册

DEVLOG 9.15 RecyclerView复用和缓存机制源码分析

2021-09-17 10:26 作者:房顶上的铝皮水塔  | 我要投稿

我是花了一天左右的时间实现了一个手写的比较简单的RecyclerView之后开始详细地看RecyclerView的缓存复用机制。个人觉得手写一个RecyclerView对于缓存复用的理解会比较有效,我也把在实现过程中遇到的问题总结在这里手写RecyclerView的问题总结和分析



当我们手指触摸到由RecyclerView渲染的ItemView的列表时,不论是向上还是向下滑动,都会有View出界并且有View加入,这个过程肯定由ViewGroup的onTouchEvent处理,所以我们理所应当先看看RecyclerView#onTouchEvent中的复用和缓存的实现:


RecyclerView#onTouchEvent:


我们直接开始进入onTouchEvent的ACTION_MOVE的事件分析。在手指进行位移的事件中,onTouchEvent调用了scrollByInternal方法。类似于我们在手写RecyclerView中的那样(这部分的内容在我这篇笔记中有分析,这里自己实现一个scroll操作也是为了避免ViewGroup的scroll对于画布的操作,毕竟我们需要模拟item划出去的操作。


果不其然,ViewGroup#scrollBy方法也被重写了,并且调用了scrollByInternal。所以我们详细看看scrollByInternal和后续的实现:


 LayoutManager#scrollStep

我们需要明确我们现在看源码的关键点,关键点在于滑动处理和复用机制,而在RecyclerView#scrollStep中需要我们关注的和缓存复用机制有关的方法就是scrollStep。

在scrollStep中,通过mLayout(其实上是一个LayoutManager对象的实例),实现了水平和垂直滑动,在RecyclerView中默认的实现返回的都是0:


当然,因为在RecyclerView中定义的LayoutManager是一个抽象类,具体的布局逻辑实际上交给了LayoutManager的子类实现:

我们可以以LinearLayoutManager为例看看垂直和横向滑动的逻辑。

# LinearLayoutManager#scrollVerticallly

LinearLayoutManager在使用上需要指定滑动的方向。LinearLayoutManager在默认情况下使用的是垂直(VERTICAL)参数。不过不论如何都会调用scrollBy。LinearLayout 重写了scrollBy方法。在scrollBy方法以及后续的调用中,Recycler这个类会作为RecyclerView复用View机制的实现。所以我们需要重点看看Recycler中对于View复用的原理。 


以上的代码调用链我归纳到了这张图中:

RecyclerView的复用机制: 概括


RecyclerView的缓存和复用机制两者是密不可分的,假设我们现在的场景是需要从已经缓存好的【容器】中寻找可用的ViewHolder,根据上图的概括,这种缓存机制可以分为四层。

相关函数都是在RecyclerView#tryGetViewHolderForPositionByDeadline中调用的。这try方法会依次调用如图所示的方法来产生holder,如果holder为空就会到下一个方法中再产生。下面稍微概括性的说说每一个方法关联的角色和作用。

Recycler#getChangedScrapViewForPosition 

这个方法从mChangedScrap中寻找可以使用的ViewHolder对象。

Recycler#getScrapOrHiddenOrCachedHolderForPosition

这个方法和下面的ForId方法都会从mAttachScrap和mCachedViews中寻找可用的ViewHolder对象,这两个对象都是ArrayList<ViewHolder>。这里需要注意的是mChangedScrap和mAttachScrap都是存储了当前仍附属在ViewGroup上的ViewHolder,这点可以从Recycler#scrapView的注释中得到。视频中说这两个对象是存储仍然在屏幕中的ViewHolder,通过手写RecyclerView的经验来分析,这样说好像有点道理,但是我没有特别详细的去看源码,所以这点存疑,姑且按照他说的来理解。

所以根据这样的划分依据,我们可以将mChangedScrap和mAttachedScrap作为一级缓存,mCachedViews作为二级缓存。

ViewCacheExtension

这个内容是由开发者自己定义的。

Adapter#createViewHolder

当以上的缓存方式中都无法提供合适的ViewHolder时,会使用Adapter创建合适的ViewHolder。


RecyclerView的缓存机制

缓存机制的入口应该分成布局和滑动,先看看布局这块的缓存处理:

LinearLayoutManager#onLayoutChildren


在LinearLayoutManager布局的过程中会考虑ViewHolder的缓存问题,具体的缓存过程的实现主要需要看定义在LayoutManager中的srapOrRecycleView方法。

具体的方法调用可以参考这个流程图,我们现在目光主要首先聚焦到LayoutManager#scrapOrRecycleView身上:

以上的代码中的两个分支都和Recycler的实例有关。这两个分支的逻辑概括起来都是缓存ViewHolder的操作。我们首先看看第一个分支中的recycleViewHolderInternal:


Recycler#recycleViewHolderInternal:

在对于缓存的ViewHolder进行缓存的过程中,Recycler会检查mCachedView的大小,如果超过出设定的大小(2),就会移除mCachedViews中的第一个元素,并且将这个移除之后的ViewHolder放置在缓存池中。这个操作的实现是由Recycler#recycleCachedViewAt实现的:

Recycler#recycleCachedViewAt

如图所示,结合上面的代码的逻辑,如果需要mCachedView放不下新的ViewHolder,就会移除第一个ViewHolder,并且将新的ViewHolder放在mCachedView后面,同时,被移除的ViewHolder1会被加入缓存池RecycledViewHolderPool中:


RecycledViewHolderPool是一个内部类,可以简单看看他的代码:


当具体要使用RecycledViewPool存储ViewHolder时,在putRecycledView中会判断RecycledViewPool的缓存池(具体是mScrap)是否存储满,存储满就不要这个ViewHolder,直接放弃;没有的话就会将这个ViewHolder通过不同的ViewType添加到对应的mScrapHeap中:

再看看这张图,ViewHolder的缓存就比较容易理解了。

所以和布局相关的缓存机制如图所示。在布局过程中,左边的逻辑会缓存划出屏幕外的View,右边会缓存仍然在屏幕内的View:

LinearLayout#fill

如果是在滑动过程中,缓存的逻辑如图所示。滑动主要处理在屏幕外的View,而不用考虑在屏幕内的View,所以没有上图右边的逻辑。



**参考内容:**


- [RecyclerView复用机制](https://www.bilibili.com/video/BV1Dp4y1t7kn?from=search&;seid=6466487146831843251&spm_id_from=333.337.0.0)


DEVLOG 9.15 RecyclerView复用和缓存机制源码分析的评论 (共 条)

分享到微博请遵守国家法律