DEVLOG 9.15 RecyclerView复用和缓存机制源码分析
我是花了一天左右的时间实现了一个手写的比较简单的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)