RecyclerView滚动时回收和复用机制

3,098 阅读7分钟

文末有答案哟~~

谈到RecyclerView的时候,复用机制是我们能脱口而出的优点之一。系统内置的ViewHolder避免了使用ListView时手动去创建ViewHolder的麻烦。关于何时回收View,何时复用View,我们能做到胸有成竹吗?当我们滑动一个RecyclerView时,是先回收View,再复用View?还是先复用View,再回收View呢?答案是都有可能。详情且看下面分析:

名词解释

1. 回收:是指View不需要再展示在屏幕中,被回收到回收池中

2. 复用:本文中的复用是指调用了onCreateViewHolder或者onBindViewHolder方法

1.滑动RV的两个场景

1.1 场景一 

RV中每个Item高度都为100px,最后一个Item超出屏幕50px。RV初始状态如下图        

场景一

Q1 假设向上滑动40px

请问是否有View发生回收和复用?如果有,先复用还是先回收?

答    回收和复用都没有发生\color{red}{答    回收和复用都没有发生}

Q2    假设向上滑动60px

请问是否有View发生回收和复用?如果有,先复用还是先回收?

答    没有发生回收,发生了复用\color{red}{答    没有发生回收,发生了复用}

Q3    假设向上滑动120px

请问是否有View发生回收和复用?如果有,先复用还是先回收?

答    发生了回收和复用。先复用后回收\color{red}{答    发生了回收和复用。先复用后回收}

1.2 场景二

RV中第一个Item高度为50px,其它都为100px,最后一个Item超出屏幕95px。RV初始状态如下

场景二

Q1    假设向上滑动40px

请问是否有View发生回收和复用?如果有,先复用还是先回收?

答    回收和复用都没有发生\color{red}{答    回收和复用都没有发生}

Q2    假设向上滑动60px

请问是否有View发生回收和复用?如果有,先复用还是先回收?

答    发生了回收,没有发生复用\color{red}{答    发生了回收,没有发生复用}

Q3    假设向上滑动120px

请问是否有View发生回收和复用?如果有,先复用还是先回收?

答    发生了回收和复用。先回收后复用\color{red}{答    发生了回收和复用。先回收后复用}

从答案可以看出。回收和复用并没有固定的答案。它因场景而异。下面我们通过案例验证答案真伪。

2. DEMO验证答案

2.1 我们来验证场景一

程序运行图

场景一运行图

程序代码

场景一代码

日志输出如下

首先进入初始状态

RecyclerView 场景一 onCreateViewHolder

RecyclerView 场景一 onBindViewHolder 0

RecyclerView 场景一 onCreateViewHolder

RecyclerView 场景一 onBindViewHolder 1

RecyclerView 场景一 onCreateViewHolder

RecyclerView 场景一 onBindViewHolder 2

RecyclerView 场景一 onCreateViewHolder

RecyclerView 场景一 onBindViewHolder 3

RecyclerView 场景一 onCreateViewHolder

RecyclerView 场景一 onBindViewHolder 4

RecyclerView 场景一 onCreateViewHolder

RecyclerView 场景一 onBindViewHolder 5

点击上滑40px。打印日志不变。证明 回收和复用都没有发生

点击上滑60px。打印日志如下。证明 没有发生回收,发生了复用

RecyclerView 场景一 onCreateViewHolder

RecyclerView 场景一 onBindViewHolder 0

RecyclerView 场景一 onCreateViewHolder

RecyclerView 场景一 onBindViewHolder 1

RecyclerView 场景一 onCreateViewHolder

RecyclerView 场景一 onBindViewHolder 2

RecyclerView 场景一 onCreateViewHolder

RecyclerView 场景一 onBindViewHolder 3

RecyclerView 场景一 onCreateViewHolder

RecyclerView 场景一 onBindViewHolder 4

RecyclerView 场景一 onCreateViewHolder

RecyclerView 场景一 onBindViewHolder 5

RecyclerView 场景一 onCreateViewHolder //只发生了复用
RecyclerView 场景一 onBindViewHolder 6

点击上滑动120px。打印日志如下。证明 发生了回收和复用。先复用后回收

RecyclerView 场景一 onCreateViewHolder

RecyclerView 场景一 onBindViewHolder 0

RecyclerView 场景一 onCreateViewHolder

RecyclerView 场景一 onBindViewHolder 1

RecyclerView 场景一 onCreateViewHolder

RecyclerView 场景一 onBindViewHolder 2

RecyclerView 场景一 onCreateViewHolder

RecyclerView 场景一 onBindViewHolder 3

RecyclerView 场景一 onCreateViewHolder

RecyclerView 场景一 onBindViewHolder 4

RecyclerView 场景一 onCreateViewHolder

RecyclerView 场景一 onBindViewHolder 5

RecyclerView 场景一 onCreateViewHolder //先复用

RecyclerView 场景一 onBindViewHolder 6

RecyclerView 场景一 发生回收 item 0 //后回收

2.2 我们来验证场景二

程序运行图

场景二运行图

程序代码

场景二代码

日志输出首先进入初始状态

RecyclerView 场景二 onCreateViewHolder

RecyclerView 场景二 onBindViewHolder 0

RecyclerView 场景二 onCreateViewHolder

RecyclerView 场景二 onBindViewHolder 1

RecyclerView 场景二 onCreateViewHolder

RecyclerView 场景二 onBindViewHolder 2

RecyclerView 场景二 onCreateViewHolder

RecyclerView 场景二 onBindViewHolder 3

RecyclerView 场景二 onCreateViewHolder

RecyclerView 场景二 onBindViewHolder 4

RecyclerView 场景二 onCreateViewHolder

RecyclerView 场景二 onBindViewHolder 5

RecyclerView 场景二 onCreateViewHolder

RecyclerView 场景二 onBindViewHolder 6

点击上滑40px。打印日志不变。证明 回收和复用都没有发生

点击上滑60px。打印日志如下。证明 发生回收,没有发生复用

RecyclerView 场景二 onCreateViewHolder

RecyclerView 场景二 onBindViewHolder 0

RecyclerView 场景二 onCreateViewHolder

RecyclerView 场景二 onBindViewHolder 1

RecyclerView 场景二 onCreateViewHolder

RecyclerView 场景二 onBindViewHolder 2

RecyclerView 场景二 onCreateViewHolder

RecyclerView 场景二 onBindViewHolder 3

RecyclerView 场景二 onCreateViewHolder

RecyclerView 场景二 onBindViewHolder 4

RecyclerView 场景二 onCreateViewHolder

RecyclerView 场景二 onBindViewHolder 5

RecyclerView 场景二 onCreateViewHolder

RecyclerView 场景二 onBindViewHolder 6

RecyclerView 场景二 发生回收 item 0 //只发生了回收

点击上滑动120px。打印日志如下。证明 发生了回收和复用。先回收后复用

RecyclerView 场景二 onCreateViewHolder

RecyclerView 场景二 onBindViewHolder 0

RecyclerView 场景二 onCreateViewHolder

RecyclerView 场景二 onBindViewHolder 1

RecyclerView 场景二 onCreateViewHolder

RecyclerView 场景二 onBindViewHolder 2

RecyclerView 场景二 onCreateViewHolder

RecyclerView 场景二 onBindViewHolder 3

RecyclerView 场景二 onCreateViewHolder

RecyclerView 场景二 onBindViewHolder 4

RecyclerView 场景二 onCreateViewHolder

RecyclerView 场景二 onBindViewHolder 5

RecyclerView 场景二 onCreateViewHolder

RecyclerView 场景二 onBindViewHolder 6

RecyclerView 场景二 发生回收 item 0 //先回收

RecyclerView 场景二 onBindViewHolder 7 //再复用

3. 滑动原理分析

参数介绍

3.1 相关参数

如图所示,介绍几个关于坐标的参数

  1. delta:手指滑动的距离120px。

  2. mOffset:RV最后一个子View的Bottom在屏幕坐标系的Y坐标600px。RV的下一个View(Item7)从mOffset处布局。

  3. mScrollingOffset:RV最后一个子view的Bottom离RV Bottom的距离50px。向上滑动不超过该距离。如超过需创建新的View填充。

  4. mVailable:delta-mScrollingOffset。可以填充View的空间。如果大于0表示有空间填充新的View

  5. 如果delta<mScrollingOffset,mScrollingOffset=delta,mVailable<0,即滑动距离小于mScrollingOffset,不要填充新的View

3.2 滑动填充和回收逻辑

滑动逻辑如下

  1.  从RecyclerView的第0个View开始遍历,直到View的Bottom>mScrollingOffset,并记录该View的下标index,回收[0,index)区间的View,index为开区间,如果index>=1,则会将[0,index)区间的View移除屏幕,并按照回收算法放入回收池。具体回收算法先按下不表。

  2. 如果mVailable>0,则从mOffset处,用新的View填充。mOffset+=新View的高度,mVailable-=新View的高度,mScrollingOffset+=新View的高度,如果mVailable<0,mScrollingOffset+=mVailable。布局完成后用步骤1的算法按需回收上面的View。

  3. 重复步骤2

  4. 将RV整体,向上移动delta或者consumed距离(一般是delta距离,但是当RecyclerView下面没有Item时会是具体消耗掉的距离)

逻辑1对应的代码如下

回收[0,1)区间的View

逻辑2填充View代码如下

addView到RecyclerView上

3.3 分析场景一

根据此滑动逻辑,我们分析场景一中的向上滑动120px

mOffset = 600px

mScrollingOffset = 50px

mAvailable = 70px

item1高度100px

  1. 首先从第0个View遍历Bottom>50px。找到item1.bottom=100px,记录index=0。因为index<1。所以不发生回收

  2. mAvailable>0,从Item6的底部,增加View Item7(此处发生复用逻辑)高度为100px,mOffset=700px,mAvailable=-30,mScrollingOffset=mScrollingOffset+100-30=120px。然后检查回收。首先从第0个View遍历Bottom>120px。找到item2.bottom=200px,记录index=1。回收[0,1)区间的View。即回收Item1

  3. mAvailable=-30<0,退出填充逻辑

  4. 整体向上移动120px

我们看到先创建Item7 然后回收Item1。跟日志相符合

RecyclerView 场景一 onCreateViewHolder //先复用
RecyclerView 场景一 onBindViewHolder 6

RecyclerView 场景一 发生回收 item 0 //后回收

同样的逻辑我们也可以分析场景二中的向上滑动120px的情况。场景二会先发生回收,再发生复用。读者可以自己去求证。

4.源码分析

RV的滑动,最终会调用LayoutManager的scrollBy方法。我们使用的是LinearLayoutManager。

  1. 代码1 updateLayoutState方法,主要是计算mOffset等参数。

  2. 代码2 fill方法,根据剩余空间,填充View

  3. 代码3 offsetChildren,整体移动RV的子View

  1. 代码1,首先判断是否需要回收View

  2. 代码2,根据剩余空间,判断是否需要填充View

  3. 代码3 是具体的layout方法

  4. 代码4 当单个layout完成后判断是否需要回收View

5. 提问互动

最后为了巩固大家对知识的理解,提出一个问题,请在评论区写出你的答案吧。

问题一 场景一的case3,向上滑动120px,120px大于第一个Item的高度100px,为何不先让Item1先回收掉呢?

问题二 谷歌这么设计的原因可能是什么?

关注字节小站公众号,回复“滚动”获取答案