RecyclerView 缓存解析

Posted by LinYaoTian on 2019-02-03

RecyclerView 和 ListView 的回收机制非常相似,但是 ListView 是以 View 作为单位进行回收,RecyclerView 是以 ViewHolder 作为单位进行回收。相比于 ListView,RecyclerView 的回收机制更为完善。

Recycler 是 RecyclerView 回收机制的实现类,他实现了四级缓存:

  • mAttachedScrap: 缓存在屏幕上的 ViewHolder。
  • mCachedViews: 缓存屏幕外的 ViewHolder,默认为 2 个。ListView 对于屏幕外的缓存都会调用 getView()
  • mViewCacheExtensions: 需要用户定制,默认不实现。
  • mRecyclerPool: 缓存池,多个 RecyclerView 共用。

要想理解 RecyclerView 的回收机制,我们就必须从其数据展示谈起,我们都知道 RecyclerView 使用 LayoutManager 管理其数据布局的显示。

RecyclerView$LayoutManager

LayoutManager 是 RecyclerView 下的一个抽象类,Google 提供了 LinearLayoutManager,GridLayoutManager 以及StaggeredGridLayoutManager 基本上能满足大部分开发者的需求。这三个类的代码都非常长,这要分析下来可了不得。本篇文章只分析 LinearLayoutManager 的一部分内容

与分析 ListView 时类似,RecyclerView 作为一个 ViewGroup,肯定也跑不了那几大过程,我们依然还是只分析其 layout 过程

[RecyclerView.java]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
dispatchLayout();
TraceCompat.endSection();
mFirstLayoutComplete = true;
}

void dispatchLayout() {
if (mAdapter == null) {
Log.e(TAG, "No adapter attached; skipping layout");
// leave the state in START
return;
}
if (mLayout == null) {
Log.e(TAG, "No layout manager attached; skipping layout");
// leave the state in START
return;
}
mState.mIsMeasuring = false;
if (mState.mLayoutStep == State.STEP_START) {
//1 没有执行过布局流程的情况
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else if (mAdapterHelper.hasUpdates()
|| mLayout.getWidth() != getWidth() ||
mLayout.getHeight() != getHeight()) {
//2 执行过布局流程,但是之后size又有变化的情况
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else {
//3 执行过布局流程,可以直接使用之前数据的情况
mLayout.setExactMeasureSpecsFrom(this);
}
dispatchLayoutStep3();
}

不过,无论什么情况,最终都是完成 dispatchLayoutStep1,dispatchLayoutStep2 和 dispatchLayoutStep3 这三步,这样的情况区分只是为了避免重复计算。

其中第二步的 dispatchLayoutStep2 是真正的布局!

1
2
3
4
5
6
7
private void dispatchLayoutStep2() {
...... // 设置状态
mState.mInPreLayout = false; // 更改此状态,确保不是会执行上一布局操作
// 真正布局就是这一句话,布局的具体策略交给了LayoutManager
mLayout.onLayoutChildren(mRecycler, mState);
......// 设置和恢复状态
}

由上面的代码可以知道布局的具体操作都交给了具体的 LayoutManager,那我们来分析其中的 LinearLayoutManager

[LinearLayoutManager.java]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
*LinearLayoutManager的onLayoutChildren方法代码也比较多,这里也不进行逐行分析
*只来看关键的几个点
*/
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler,
RecyclerView.State state) {

......
//状态判断以及一些准备操作
onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
/**
*1 感觉这个函数应该跟上一篇我们所分析的ListView的detachAllViewsFromParent();有点像
*/
detachAndScrapAttachedViews(recycler);
......
//2 感觉这个函数跟上一篇我们所分析的ListView的fillUp有点像
fill(recycler, mLayoutState, state, false);

}

上面就是真正的布局代码,跟分析 ListView 一样,下面我们开始分析 RecyclerView 的两次 Layout 过程。

Layout

第一次 Layout

从上面的 onLayoutChildren() 可以看出,这里有两个比较重要的函数:detachAndScrapAttachedViews()fill(recycler, mLayoutState, state, false)

第 1 个重要函数

[RecyclerView$LayoutManager]

1
2
3
4
5
6
7
8
9
10
11
12
13
/	**
* 暂时detach和scrap所有当前附加的子视图。视图将被丢弃到给定的回收器中(即参数recycler)。
* 回收器(即Recycler)可能更喜欢重用scrap的视图。
*
* @param recycler 指定的回收器Recycler
*/
public void detachAndScrapAttachedViews(Recycler recycler) {
final int childCount = getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
final View v = getChildAt(i);
scrapOrRecycleView(recycler, i, v);
}
}

在第一次 Layout 时,onLayoutChildren() 会尝试调用 detachAndScrapAttachedViews(recycler) ,将 RecyclerView 中的 View 清除,但是第一次 Layout 时 RecyclerView 是没有 View,所以可以跳过该函数。

第 2 个重要函数

[LinearLayoutManager.java]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
......
int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
while ((layoutState.mInfinite || remainingSpace > 0)
&& layoutState.hasMore(state)) {//这里循环判断是否还有空间放置item
......
//真正放置的代码放到了这里
layoutChunk(recycler, state, layoutState, layoutChunkResult);
......
}
if (DEBUG) {
validateChildOrder();
}
return start - layoutState.mAvailable;
}

跟进 layoutChunk

[LinearLayoutManager.java]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
/**
*获取一个View,这个函数应该是重点了,
*/
View view = layoutState.next(recycler);
......
//添加View
addView(view);
......
//计算View的大小
measureChildWithMargins(view, 0, 0);
......
//布局
layoutDecoratedWithMargins(view, left, top, right, bottom);
......
}

继续跟进 next()

[LinearLayoutManager$LayoutState]

1
2
3
4
5
6
7
8
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}

getViewForPosition()方法可以说是 RecyclerView 中缓存策略最重要的方法,该方法是从 RecyclerView 的回收机制实现类Recycler 中获取合适的 View,或者新创建一个 View。

1
2
3
4
5
6
7
View getViewForPosition(int position, boolean dryRun) {
/**
*从这个函数就能看出RecyclerView是以ViewHolder为缓存单位的些许端倪
*/
return tryGetViewHolderForPositionByDeadline
(position, dryRun, FOREVER_NS).itemView;
}

跟进 tryGetViewHolderForPositionByDeadline()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/**
*试图获得给定位置的ViewHolder,无论是从
*mAttachedScrap、mCachedViews、mViewCacheExtensions、mRecyclerPool、还是直接创建。
*
* @return ViewHolder for requested position
*/
@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
......
// 1) 尝试从mAttachedScrap获取
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
......
}

if (holder == null) {
......
final int type = mAdapter.getItemViewType(offsetPosition);
// 2) 尝试从mCachedViews获取
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
if (holder != null) {
// update position
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
}
}

// 3) 尝试从mViewCacheExtensions获取
if (holder == null && mViewCacheExtension != null) {
......
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
......
}
}

// 4) 尝试从mRecyclerPool获取
if (holder == null) { // fallback to pool

holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
if (holder == null) {
// 5) 直接创建
holder = mAdapter.createViewHolder(RecyclerView.this, type);

}
}

......
// 6) 判断是否需要bindHolder
if (!holder.isBound()
|| holder.needsUpdate()
|| holder.isInvalid()) {

final int offsetPosition = mAdapterHelper.findPositionOffset(position);
bound = tryBindViewHolderByDeadline
(holder, offsetPosition, position, deadlineNs);
}
......

return holder;
}

那么在第 1 次 layout 时,前 4 步都不能获得 ViewHolder,那么进入第 5, 直接创建,也就是调用 Adapter 中的 onCreateViewHolder() 。初次创建完毕后,便会进入 6 ,导致我们重写的 onBindViewHolder() 被回调,让数据和 View 进行绑定。

第二次 Layout

从 ListView 中我们就知道了再简单的 View 也至少需要两次 Layout,在 ListView 中通过把屏幕的子 View detach 并加入mActivieViews,以避免重复添加 item 并可通过 attach 提高性能,那么在 RecyclerView 中,它的做法与 ListView 十分类似,RecyclerView 也是通过 detach 子 View,并把子 View 对应的 ViewHolder 加入其 1 级缓存 mAttachedScrap。这部分我们就不详细分析了,读者可参照上一篇的步骤进行分析。

RecycledViewPool 的结构

RecycledViewPool 是 RecyclerView 复用机制中的最低级缓存,如果在这里面还找不到想要的缓存,则会重新创建一个新的 View,同时 RecycledViewPool 还是所有 RecyclerView 共享的,默认每种 ViewType 最多只能缓存 5 个。而且从这里复用的 ViewHolder 是针对 ViewType 的,因此需要重新绑定数据。

[RecyclerView.java]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public static class RecycledViewPool {
// 默认每种 ViewType 缓存最多存储 5 个
private static final int DEFAULT_MAX_SCRAP = 5;
// 每种 ViewType 的缓存数据结构
static class ScrapData {
ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
long mCreateRunningAverageNs = 0;
long mBindRunningAverageNs = 0;
}
//存储缓存的容器
SparseArray<ScrapData> mScrap = new SparseArray<>();

private int mAttachCount = 0;

public void clear() {
...
}

public void setMaxRecycledViews(int viewType, int max) {
...
}

/**
* 从缓存池中获取一个缓存,如果获取成功,会把它从缓存池中删除
*/
public ViewHolder getRecycledView(int viewType) {
final ScrapData scrapData = mScrap.get(viewType);
if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
return scrapHeap.remove(scrapHeap.size() - 1);
}
return null;
}

/**
* 与前面的 getRecycledView() 不同,获取后不会删除缓存
*/
private ScrapData getScrapDataForType(int viewType) {
ScrapData scrapData = mScrap.get(viewType);
if (scrapData == null) {
scrapData = new ScrapData();
mScrap.put(viewType, scrapData);
}
return scrapData;
}


public void putRecycledView(ViewHolder scrap) {
final int viewType = scrap.getItemViewType();
final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
return;
}
if (DEBUG && scrapHeap.contains(scrap)) {
throw new IllegalArgumentException("this scrap item already exists");
}
scrap.resetInternal();
scrapHeap.add(scrap);
}

void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter,
boolean compatibleWithPrevious) {
if (oldAdapter != null) {
detach();
}
if (!compatibleWithPrevious && mAttachCount == 0) {
clear();
}
if (newAdapter != null) {
attach(newAdapter);
}
}
}

总结

Recycler 有4个层次用于缓存 ViewHolder 对象,优先级从高到底依次为 mAttachedScrap、mCachedViews、mViewCacheExtension、mRecyclerPool。如果四层缓存都未命中,则重新创建并绑定 ViewHolder 对象

缓存性能:

缓存 重新创建ViewHolder 重新绑定数据
mAttachedScrap false false
mCachedViews false false
mRecyclerPool false true

缓存容量:

  • mAttachedScrap:没有大小限制,但最多包含屏幕可见表项。
  • mCachedViews:默认大小限制为2,放不下时,按照先进先出原则将最先进入的ViewHolder存入回收池以腾出空间。
  • mRecyclerPool:对ViewHolderviewType分类存储(通过SparseArray),同类ViewHolder存储在默认大小为5的ArrayList中。

缓存用途:

  • mAttachedScrap:用于布局过程中屏幕可见表项的回收和复用。
  • mCachedViews:用于移出屏幕表项的回收和复用,且只能用于指定位置的表项,有点像“回收池预备队列”,即总是先回收到mCachedViews,当它放不下的时候,按照先进先出原则将最先进入的ViewHolder存入回收池。
  • mRecyclerPool:用于移出屏幕表项的回收和复用,且只能用于指定viewType的表项

缓存结构:

  • mAttachedScrapArrayList<ViewHolder>
  • mCachedViewsArrayList<ViewHolder>
  • mRecyclerPool:对ViewHolderviewType分类存储在SparseArray<ScrapData>中,同类ViewHolder存储在ScrapData中的ArrayList