RecyclerView的缓存机制(面试常客)

在构建滚动列表时,我们常首选RecyclerView,出于它优秀的缓存复用机制。

核心机制

RecyclerView的缓存机制又称回收复用机制,RecyclerView构建列表视图分为以下三步:

第一步的创建ViewHolder是RecyclerView构建视图时最耗时的操作,而RecyclerView正是因为做到了缓存复用,从而减少调用创建ViewHolder的次数。

当列表被创建时结构如下,每个ViewHolder中的A、B代表不同的View种类(ViewType):

当我们滚动第一个ViewHolder(此处简称p0)到屏幕以外时,它会被存入mCachedViews中:

此处被回收的p0依旧保存着之前的数据,各种内部状态没有被重置。

如果我们向上滚动,被缓存的p0会从mCachedViews缓存区中移回列表:

因此不需要重复CreateViewHolder和BindViewHolder,减少了耗时。

可见,缓存区是用来存储最近离开屏幕区的ViewHolder。 这些ViewHolder因为出于屏幕区的边界,更容易因为用户的滚动和抖动被重新显示,因此更需要被快速的加载到视图中。

缓存区存储ViewHolder的默认大小为2,开发者可根据实际需求更改大小。

当缓存区已满,而有新的ViewHolder被放入缓存区时,最先被置入缓存区的ViewHolder会被移到mRecyclerPool中:

进入mRecyclerPool的ViewHolder各种内部状态会被重置,如position会被置为-1,可理解为与之前的数据已解绑;图中的A和B表示不同的ViewType,每个能存储5个ViewHolder,这个大小也是可以被修改的。

分析完列表移出ViewHolder的过程,我们反过来看移入ViewHolder的:

假设此时有一个新的类型为B的ViewHolder(简称p7)要移入屏幕区。

首先查看缓存区中是否有可以复用的ViewHolder。这是因为被放入此处的ViewHolder是不需要重置数据的,因此优先考虑这里的ViewHolder,如果有相同的position,就会被取出复用。此处的逻辑和前面p0重新移入的逻辑相同;

图中的例子position=7,与缓存区中的两个ViewHolder的position都不相同,因此无法复用;接下来查看循环池中的ViewHolder。因为View类型为B的循环池中没有ViewHolder,所以依然无法复用;此时我们只能重新走Create和Bind的方法。

如果循环池中有相同类型的ViewHolder:

会从循环池中取出复用,因为被放入循环池中的ViewHolder都是被重置了的,虽然不用走Create方法,但是重新的Bind还是要有的。

是否可以只用一种ViewType?

对于不同的ViewType我们有不同的循环池,那我们是否可以只用一种ViewType,延长这一个循环池,提高回收复用率?

首先是可读性和可维护性会变差。因为不同的ViewType通常数据结构和视图样式方式不同,如果为了让一种ViewType都适配他们,代码肯定会变得很乱。

其次是不用懒加载,增加调用耗时;使用懒加载,无法避免视图切换耗时。首先因为ViewType的数据结构和视图样式等诸多内容不尽相同,如果一次全部加载内存开销必然很大,因此我们使用懒加载的方式最为合适,只有需要的时候才取出使用;

但因为我们把原本应该设计为不同的ViewType都塞进了一个ViewType,导致会一次加载所有原本不同的ViewType,无法使用懒加载避免这一现象;

而同时因为ViewType视图样式不同,肯定要重新inflate不同的视图样式,根本没有减少耗时。

RecyclerView缓存机制失效的情况

重写Adapter的onFailedToRecycleView

首先查看RecyclerView的源码:

在是否回收ViewHolder的判断中包含两个条件:boolean值forceRecycle和isRecyclable方法。我们首先查看后者代码:

后者的isRecyclable方法获取到的是一个名为TransientState的值(直译:临时状态)。当某个ViewHolder中的某个View在做属性动画时,这个值会属性动画开始和结束时被设置:

这么来说,如果一个ViewHolder在离开显示区时,同时内部有任意一个view在做属性动画,TransientState的值就会是false,isRecyclable方法返回的也是false,这个ViewHolder就无法被存储到缓存区或循环池中。

这种情况最容易出现在ViewHolder中存在循环属性动画的场景。如果没有对这个循环属性动画做处理,那么就会出现缓存机制失效的情况。如dy中的专辑旋转动画:

而是否回收ViewHolder的判断中,前者forceRecycle是根据onFailedToRecycleView方法设定的,我们可以重写这个方法:

Kotlin 复制代码
override fun onFailedToRecycleView(holder: MyViewHolder?): Boolean {
    return true
}

onBindViewHolder中重置View的动画属性

为什么Android要设计ViewHolder中的View的属性动画不能被回收,要我们如此大费周章?

这么设计的原因是防止ViewHolder被复用时之前的动画状态被保留。因此我们可以在重新绑定ViewHolder的onBindViewHolder方法中,重置View的动画属性,如偏转角度(Rotation)、透明度(Alpha)等。

RecyclerView的特殊复用机制

循环池(mRecyclerPool)可以服务于多个RecyclerView。当其他RecyclerView中拥有循环池中相同的ViewType时,他们都可以使用这个循环池。

参考

Android RecyclerView的缓存机制_哔哩哔哩_bilibili

相关推荐
懒惰的cv怪1 小时前
Redis学习 ——缓存
redis·学习·缓存
ketil271 小时前
Redis 典型应用 - 缓存(cache)
数据库·redis·缓存
q567315235 小时前
用 PHP或Python加密字符串,用iOS解密
java·python·ios·缓存·php·命令模式
只是有点小怂6 小时前
直接缓存映射Direct Mapped Cache
缓存
小兜全糖(xdqt)6 小时前
nuget 管理全局包、缓存和临时文件夹
缓存
Allen Bright6 小时前
Redis介绍
数据库·redis·缓存
只是有点小怂8 小时前
缓存冲突(Cache Conflict)
缓存
Allen Bright9 小时前
Redis安装
数据库·redis·缓存
哭哭啼15 小时前
Redis环境部署(主从模式、哨兵模式、集群模式)
数据库·redis·缓存
只是有点小怂18 小时前
受害者缓存(Victim Cache)
缓存