0%

[原创]简述 RecyclerView 的重用机制

前言

最近一直在用 Kotlin 重写项目通用框架,并加上了完整的示例(项目已开源,还在不断持续更新中添加新功能,这是下载地址)。一来可以更好的学习 Kotlin,再者还可以学习下 Android 的最新技术和最新版本的变化。其中在展示 RecyclerView 的常用功能时,关于重用机制又发现了一些需要注意的新东西,所以特此记录并分享下心得。

重用机制

强烈建议大家在阅读本文之前,先看一下我之前写的“RecyclerView ViewHolder 关于 Item 位置相关的不同属性的区别”。

本文主要是讲解 RecyclerView 的重用机制,对于常见用法大家可以自行参考开源项目中的示例。虽然已经有很多现成的轮子可供大家直接拿来用,但是依然有必要对基础知识加以学习。毕竟了解了内部原理才能更好的解决日后出现的各种问题。

这篇文章非常形象的图解了 RecyclerView 的重用机制,建议大家先看一下。

recyclerview-cache

该图引自刚才推荐的文章,图中清楚的表明了 RecyclerView 重用机制是如何工作的。在阅读 RecyclerView 源码时,请特别重要 androidx.recyclerview.widget.RecyclerView.Recycler 这个类。主要的成员变量的默认值,都是在这个类里的。

我们平时接触最多的就是第一级缓存和第三级缓存,一级缓存大小默认值为 2, 三级缓存按 type 区分,每个 type 的缓存默认值为 5

下面让我们来具体看一个实例:

recycler-view-example

上面这张图是一个非常简单的 RecyclerView 显示数据的例子。大家可以看到,屏幕可见区域内,显示了 7 条数据。再来看一下此时的 Log

recycler-view-example-init-data-log

通过日志可以看出,虽然屏幕上只显示了 7 条数据,但是实际 Adapter 却生成了 8 条数据,因为第 8 条数据是即将显示在屏幕上的。再来看下 Reuse tag 值都是 null 说明此时并没有任何 ViewHolder 被回收。

接着,我们向下滑动 RecyclerView 显示更多的数据,此时的日志如下:

recycler-view-example-recycle

我们可以看到,onViewRecycled 回调被调用了,说明有 ViewHolder 被回收了(Demo String1Demo String2Demo String3Demo String4 被回收了)。我们在被回收的 ViewHolder 上记录个 tag(标记),这样当它被重用时,我们就知道重用的哪一个。

接下来,我们再向上滑动 RecyclerView(也就是往回滑动),通过下面日志中的 tag 我们可以看到,当显示 Demo String3Demo String2Demo String1 这三条数据时,并没有重新生成新的 ViewHolder,而是重用原来的 ViewHolder

recycler-view-example-reuse

我们再继续向下滑动时,虽然加载了更多的数据,但都是重用之前的 ViewHolder,并没有再生成新的 ViewHolder。如下图所示:

recycler-view-example-reuse2

这就是 RecyclerView 显示高效的原因。

重用机制引来的问题“副作用”

重用机制虽然好,但是却会给我们带来一些小麻烦。想必大家再刚开始使用 RecyclerView 时一定都遇到过下面这样的问题:Item 的常见布局中,左侧一般都会显示一张图片,并且在 RecyclerView 进入编辑状态时,最左侧通常还会再显示一个 RadioButtonCheckBox 用于多选。当我们向下滑动数据时,左侧的图片会有被重复显示的现象;若我们选中一些 RadioButtonCheckBox 后,再向下滑动 RecyclerView 时,你会发现后面的一些 Item 也被莫名奇妙的选中了。这是为什么呢?

如果你仔细看过我刚才写的实例说明,那么你大致应该会明白为什么会有上面这些奇怪的问题了。没错,原因正是重用机制导致的。如果我们在代码层面不做一些特殊处理,只是简单的为 ViewHolder 赋值的话,肯定是会出上面的问题的。原因很简单,之前的实例也提到了,RecyclerView 会一直重用那些不再被显示的 ViewHolder,并不会用每一条新数据创建一个新的 ViewHolder。如果 Item1CheckBox 之前是处于被选中状态的话,那么当它被重用后,自然也是处于被选中的状态,这就导致了会被“误选”的原因。图片会被“重复”显示也是一个道理,因为图片通常是被异步加载的,当之前已经被加载过图片的 ImageView 被重用时,若新的图片还没有被加载,就会出现旧的图片因为“重用机制”导致被“重复”显示的问题。

解决上述问题的办法也很多,大家 Google 一下很快就会解决这些问题。不过有一些做法表面上看虽然有效,但是可能会破坏“重用机制”,导致显示大幅性能,甚至还会导致 OOM。由于这里有必要详细说一下。

解决“重用机制”的“副作用”

先说一下“RadioButton”或“CheckBox”被“误选”的解决办法。这里我不评论网上的做法哪些好,哪些坏,仅讲解笔者自己用到的解决办法。具体做法如下:在每个 Item 的数据 Bean 里添加一个“选中状态”变量,并在 onBindViewHolder 回调中根据“选中状态”的值来重新为“RadioButton”或“CheckBox”赋值。这样就可以解决“误选”问题,并且该方法也不会破坏“重用机制”。

上网查过资料的人可能会说:“有个更简单的办法,根据不需要添加‘变量’,只需要覆写‘getItemViewType(position: Int)’方法,直接返回 position 就可以解决这个问题”。没错,这样确实会解决这个问题,不光能解决这个问题,所有“重用机制”的“副作用”都可以被解决,这难道不是最优雅的解决办法吗?要是真这么简单就可以解决这个问题,Android 早就自行解决了。

看一下“getItemViewType(position: Int)”的注释就能明白,覆写该方法并返回 position 会直接破坏“重用机制”,导致无法 ViewHolder 无法被“重用”(该问题很容易被验证,覆写下该方法然后向之前实例那些打日志就可以验证该问题),因为每一个 Item 都有了一个唯一的 View Type,显示 Item 时,就会为每一个 Item 生成一个新的 ViewHolder ,就这是表面上“副作用”被解决的原因,但这样也完美的破坏了“重用机制”,不能被重用,RecyclerView的显示性能会大幅下降。当加载大量数据后,很容易就产生 OOM。因此若要采取该方法解决“副作用”一定要注意加载的数据量不能过多。

再来说下图片被“重复”显示的问题。解决该问题方法很多,虽然也可以采用和刚才添加变量的方法来解决该问题,不过解决图片被“重复”显示的话,像刚才那么就显示有些麻烦了。无法是为 ImageView 添加 tag,还是添加变量,本质上都是为了记一个状态,然后当有数据需要显示在页面上时,根据该变量的值,来判断是否需要重新设置新值。但是我们可以采取更加简单的方法:覆写“getItemId(position: Int)”方法。

不需要改动任何业务代码,仅通过覆写“getItemId(position: Int)”的方式就可以完美的解决图片被“重复显示”的问题,而且最重要的是这样做还不会破坏“重用机制”,导致完美的解决方案。

如何优化

直接上图说明解决办法。(点击此处查看原始图片来源)

recyclerview-optimize

recyclerview-optimize-way

源代码

需要源码的朋友,可以从我的 Gitee 上下载。

参考文献

坚持原创及高品质技术分享,您的支持将鼓励我继续创作!