Recyclerview回收复用机制——图文详解

前言

最近在开发大图预览页时,利用了Recyclerview滑动特性,将每张图片作为一个itemView,布局为填充整个屏幕(即一个itemView占据了整个屏幕),示意图如下: 这么做的好处是,无需设置复杂的滑动手势逻辑。但在交付体验时发现,当用户滑动很快时,itemView加载的速度较慢,除了网速原因还有一个不可忽视的case是RV的缓存机制: mCachedView的默认容量为2,mRecyclerPool的默认缓存容量为5 快速滑动时,mCachedView的容量很快就会被消耗,从而将ViewHolder放到mRecyclerPool缓存中,再次获取对应的itemView时又需要重新onCreateViewHolder和onBindViewHolder,这样的耗时又增加了很多 解决方法也好办,可以考虑扩充下mCachedView的默认容量,即

kotlin 复制代码
recyclerView.setItemViewCacheSize(5) // 扩充mCachedView的默认容量

借着这次需求,刚好我们可以详细复习一下Recyclerview的缓存机制。于是有了此文。

一、核心机制

RecyclerView的缓存机制主要由四级缓存构成,分别是mAttachedScrap、mCachedViews、mViewCacheExtension和mRecyclerPool 。每一级缓存都有其独特的功能和适用场景,它们相互协作,共同实现高效的视图缓存与复用。 RecyclerView的缓存机制又称回收复用机制,RecyclerView构建列表视图分为以下三步:

  1. Create View Holder,创建ViewHolder
  2. Bind View Holder,绑定ViewHolder数据
  3. Render View Holder,ViewHolder渲染到页面 第一步的创建ViewHolder是RecyclerView构建视图时最耗时的操作,而RecyclerView正是因为做到了缓存复用,从而减少调用创建ViewHolder的次数。

二、缓存区

2.1 mCachedView

当列表被创建时结构如下,每个ViewHolder中的A、B代表不同的View种类(ViewType): 当我们滚动第一个ViewHolder(此处简称p0)到屏幕以外时,它会被存入mCachedViews 中: 此处被回收的p0依旧保存着之前的数据,各种内部状态没有被重置。 如果我们向上滚动,被缓存的p0会从mCachedViews缓存区中移回列表: 因此不需要重复CreateViewHolder和BindViewHolder,减少了耗时。 可见,缓存区是用来存储最近离开屏幕区的ViewHolder。 这些ViewHolder因为出于屏幕区的边界,更容易因为用户的滚动和抖动被重新显示,因此更需要被快速的加载到视图中。 缓存区存储ViewHolder的默认大小为2,开发者可根据实际需求更改大小。

2.2 mRecyclerPool

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

2.3 移入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还是要有的。

Q & A

为什么不能随意扩大cachedView的默认容量

  • 内存消耗显著上升 mCachedViews 中的 ViewHolder 会完整保留 View 实例及其子 View(如 ImageView、TextView 等)的引用,且不会释放绑定的数据(如图片 Bitmap、文本内容)。若容量过大(如超过 20),尤其是 Item 布局复杂或包含大图时,会导致内存占用激增,可能引发 OOM(内存溢出),尤其是在低内存设备上。
  • 缓存管理开销增加 mCachedViews 采用 LRU(最近最少使用) 策略淘汰旧缓存:当容量满时,每次新增 ViewHolder 都需要移除最久未使用的实例。容量过大时,LRU 维护的链表操作(插入、删除)会消耗更多 CPU 资源,反而可能降低性能。
  • 可能降低 RecycledViewPool 的利用率 mCachedViews 和 RecycledViewPool 存在 "竞争关系":ViewHolder 优先进入 mCachedViews,只有当 mCachedViews 满了,才会被移至 RecycledViewPool。若 mCachedViews 容量过大,会导致 RecycledViewPool 中的缓存减少,而 RecycledViewPool 的缓存是跨 ViewType 共享的(在多列表场景下更有价值),间接降低整体缓存效率。

RecyclerPool是否可以只用一种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时,他们都可以使用这个循环池。

相关推荐
牛蛙点点申请出战4 小时前
仿微信语音 WaveView -- Compose 实现
android·前端
小林rush4 小时前
深入 Vue3 编译原理:实现一个mini模板编译器
前端·vue.js·前端框架
水冗水孚4 小时前
每天进步一点点——学习高度过渡的四种方式
前端·javascript·css
跟橙姐学代码4 小时前
Python 调试的救星:pdb 帮你摆脱“打印地狱”
前端·pytorch·python
Olaf_n4 小时前
类加载器与运行时数据区
前端
沐怡旸4 小时前
【底层机制】std::move 解决的痛点?是什么?如何实现?如何正确用?
c++·面试
excel5 小时前
PM2 Cluster 模式下 RabbitMQ 队列并行消费方案
前端·后端
IT_陈寒6 小时前
React性能优化:这5个被90%开发者忽略的Hooks用法,让你的应用快3倍
前端·人工智能·后端
山有木兮木有枝_17 小时前
前端性能优化:图片懒加载与组件缓存技术详解
前端·javascript·react.js