RecyclerView

RecyclerView通过LayoutManager进行测量和布局,通过Recycler进行复用viewholder


  • setLayoutManager:必选项,设置 RV 的布局管理器,决定 RV 的显示风格。常用的有线性布局管理器(LinearLayoutManager)、网格布局管理器(GridLayoutManager)、瀑布流布局管理器(StaggeredGridLayoutManager)。

  • setAdapter:必选项,设置 RV 的数据适配器。当数据发生改变时,以通知者的身份,通知 RV 数据改变进行列表刷新操作。

  • addItemDecoration:非必选项,设置 RV 中 Item 的装饰器,经常用来设置 Item 的分割线。

  • setItemAnimator:非必选项,设置 RV 中 Item 的动画。


onMeasure

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
final boolean measureSpecModeIsExactly =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
//表示在 XML 布局文件中,RV 的宽高被设置为 match_parent 或者具体值,那么直接将 measureSpecModeIsExactly 置为 true,并调用 mLayout(传入的 LayoutManager)的 onMeasure 方法测量自身的宽高即可。
if (measureSpecModeIsExactly || mAdapter == null) {
return;
}

……

mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
dispatchLayoutStep2();
//表示在 XML 布局文件中,RV 的宽高设置为 wrap_content,则会执行下面的 dispatchLayoutStep2(),其实就是测量 RecyclerView 的子 View 的大小,最终确定 RecyclerView 的实际宽高。
// now we can get the width and height from the children.
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);

onLayout

调用了一层 dispatchLayout() 方法,如果在 onMeasure 阶段没有执行 dispatchLayoutStep2() 方法去测量子 View,则会在 onLayout 阶段重新执行。

1
2
3
4
5
private void dispatchLayoutStep2() {
……
mLayout.onLayoutChildren(mRecycler, mState);
……
}

核心逻辑是调用了 mLayout 的 onLayoutChildren 方法。这个方法是 LayoutManager 中的一个空方法,主要作用是测量 RV 内的子 View 大小,并确定它们所在的位置。LinearLayoutManager、GridLayoutManager,以及 StaggeredLayoutManager 都分别复写了这个方法,并实现了不同方式的布局。

以 LinearLayoutManager 的实现为例

1.在 onLayoutChildren 中调用 fill 方法,完成子 View 的测量布局工作;

2.在 fill 方法中通过 while 循环判断是否还有剩余足够空间来绘制一个完整的子 View;

layoutChunk 是一个非常核心的方法,这个方法执行一次就填充一个 ItemView 到 RV,部分源码如下:

1
2
3
4
5
6
View view = layoutState.next(recycler);
addView(view);
……
measureChildWithMargins(view, 0, 0);
……
layoutDecoratedWithMargins(view, left, top, right, bottom);

onDraw

1
2
3
4
5
6
7
8
9
@Override
public void onDraw(Canvas c) {
super.onDraw(c);

final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDraw(c, this, mState);
}
}

如果有添加 ItemDecoration,则循环调用所有的 Decoration 的 onDraw 方法,将其显示。

缓存复用原理 Recycler

1
2
3
4
5
6
7
8
9
10
11
12
public final class Recycler {
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
ArrayList<ViewHolder> mChangedScrap = null;

final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();

RecycledViewPool mRecyclerPool;

private ViewCacheExtension mViewCacheExtension;

……
}
第一级缓存 mAttachedScrap&mChangedScrap

是两个名为 Scrap 的 ArrayList,这两者主要用来缓存屏幕内的 ViewHolder。

为什么屏幕内的 ViewHolder 需要缓存呢?
通过下拉刷新列表中的内容,当刷新被触发时,只需要在原有的 ViewHolder 基础上进行重新绑定新的数据 data 即可,而这些旧的 ViewHolder 就是被保存在 mAttachedScrap 和 mChangedScrap 中。实际上当我们调用 RV 的 notifyXXX 方法时,就会向这两个列表进行填充,将旧 ViewHolder 缓存起来。

第二级缓存 mCachedViews

它用来缓存移除屏幕之外的 ViewHolder,默认情况下缓存个数是 2,不过可以通过 setViewCacheSize 方法来改变缓存的容量大小。如果 mCachedViews 的容量已满,则会根据 FIFO 的规则将旧 ViewHolder 抛弃,然后添加新的 ViewHolder

第三级缓存 ViewCacheExtension
第四级缓存 RecycledViewPool

RecycledViewPool 同样是用来缓存屏幕外的 ViewHolder,当 mCachedViews 中的个数已满(默认为 2),则从 mCachedViews 中淘汰出来的 ViewHolder 会先缓存到 RecycledViewPool 中。ViewHolder 在被缓存到 RecycledViewPool 时,会将内部的数据清理,因此从 RecycledViewPool 中取出来的 ViewHolder 需要重新调用 onBindViewHolder 绑定数据。

多个 RV 之间可以共享一个 RecycledViewPool,这对于多 tab 界面的优化效果会很显著。需要注意的是,RecycledViewPool 是根据 type 来获取 ViewHolder,每个 type 默认最大缓存 5 个。因此多个 RecyclerView 共享 RecycledViewPool 时,必须确保共享的 RecyclerView 使用的 Adapter 是同一个,或 view type 是不会冲突的。

RV 是如何从缓存中获取 ViewHolder 的

在 layoutChunk 方法中通过调用 layoutState.next 方法拿到某个子 ItemView,然后添加到 RV 中。

最终调用 tryGetViewHolderForPositionByDeadline 方法来查找相应位置上的ViewHolder,在这个方法中会从上面介绍的 4 级缓存中依次查找

如果在各级缓存中都没有找到相应的 ViewHolder,则会使用 Adapter 中的 createViewHolder 方法创建一个新的 ViewHolder。

何时将 ViewHolder 存入缓存

第一次 layout

当调用 setLayoutManager 和 setAdapter 之后,RV 会经历第一次 layout 并被显示到屏幕上,此时并不会有任何 ViewHolder 的缓存,所有的 ViewHolder 都是通过 createViewHolder 创建的。

刷新列表

如果通过手势下拉刷新,获取到新的数据 data 之后,我们会调用 notifyXXX 方法通知 RV 数据发生改变,这回 RV 会先将屏幕内的所有 ViewHolder 保存在 Scrap 中,当缓存执行完之后,后续通过 Recycler 就可以从缓存中获取相应 position 的 ViewHolder(姑且称为旧 ViewHolder),然后将刷新后的数据设置到这些 ViewHolder 上,最后再将新的 ViewHolder 绘制到 RV 上