RecycleView的缓存原理

RecycleView的缓存原理

上一篇文章RecycleView的绘制流程,当我们走到子View的布局流程的layoutChunk方法的时候,通过View view = layoutState.next(recycler);方法获取将要布局的子View,然后进行后续操作,现在来看一下这个子View是怎么获取的。

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;
}

可以看到是通过recycler.getViewForPosition这个方法获取的

1
2
3
4
5
6
public View getViewForPosition(int position) {
return getViewForPosition(position, false);
}
View getViewForPosition(int position, boolean dryRun) {
return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}

tryGetViewHolderForPositionByDeadline方法返回的是一个ViewHolder对象,然后直接返回ViewHolder的成员变量itemView,也就是当前将要布局的子view。

到这里还没有涉及到缓存的代码,我们也可以猜测,RecycleView的缓存,不只是对View缓存,是对ViewHolder的缓存。

tryGetViewHolderForPositionByDeadline这个方法是真正的缓存机制的入口,它是RecycleView.Recycler中的方法,按照我们正常的思维想想里面肯定是先从缓存中去,取不到在新建一个,那么这个缓存是啥呢,在进入tryGetViewHolderForPositionByDeadline方法之前我们先看几个Recycler中的成员变量方便看下面的代码

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

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

private final List<ViewHolder>
mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);

private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
int mViewCacheMax = DEFAULT_CACHE_SIZE;

RecycledViewPool mRecyclerPool;

private ViewCacheExtension mViewCacheExtension;

static final int DEFAULT_CACHE_SIZE = 2;
......

这几个就是用来缓存的List了

  • mAttachedScrap:不参与复用,只保存在重新布局的时候,从RecycleView中剥离的当前在显示的ViewHolder列表。比如当我们插入一条或者删除一条数据,这时候需要重新布局,怎么办呢,办法就是把当前屏幕上显示的view先拿下来保存到一个列表中,然后在重新布局上去。这个列表就是mAttachedScrap。所以它只是存储重新布局前从RecycleView上剥离出的ViewHolder,并不参与复用
  • mUnmodifiableAttachedScrap:通过Collections.unmodifiableList(mAttachedScrap),把mAttachedScrap放进去,返回一个不可更改的列表,共外部获取
  • mChangedScrap:不参与复用 从新布局的时候要修改的放到这里面,其余的放到mAttachedScrap
  • mCachedViews:从名字就能看出来这就是参与缓存的list
  • mRecyclerPool:参与缓存,并且它里面的ViewHolder的信息都会被重置,相当于一个新建的ViewHolder,供后面使用
  • mViewCacheExtension:这个是让我们自己扩展自己的缓存策略,一般情况下我们不会自己写这东西的。

所以,mCachedViews ,mRecyclerPool , mViewCacheExtension 这三个组成了一个三级缓存,当RecyclerView要拿一个复用的ViewHolder的时候,查找的顺序是mCachedViews->mViewCacheExtension->mRecyclerPool。因为一般情况下我们不会写mViewCacheExtension,所以一般情况就两级缓存mCachedViews->mRecyclerPool

实际上mCachedViews是不参与真正的回收的,mCachedViews的作用是保存最新被移除的ViewHolder,通过removeAndRecycleView(view, recycler)方法,它的作用是,当需要更新ViewHoder的时候,精确的匹配是不是刚才移除那个,如果是直接拿出来让RecycleView布局,如果不是,即使它中存在ViewHolder,也不会返回,而是去mRecyclerPool中找一个新的ViewHolder然后重新赋值。mAttachedScrap中也是精确匹配步骤跟mCachedViews一样。

OK下面我们进入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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
if (position < 0 || position >= mState.getItemCount()) {
throw new IndexOutOfBoundsException("Invalid item position " + position
+ "(" + position + "). Item count:" + mState.getItemCount()
+ exceptionLabel());
}
boolean fromScrapOrHiddenOrCache = false;
ViewHolder holder = null;
// 0) 是否处于布局前的状态 去mChangedScrap中找,布局前的状态也就是重新布局的时候
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
// 1) 先从mAttachedScrap中找,找不到在去mCachedViews中查找
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if (holder != null) {
if (!validateViewHolderForOffsetPosition(holder)) {
// 检查是不是我们要查找的viewholder,如果不是移除
if (!dryRun) {
// we would like to recycle this but need to make sure it is not used by
// animation logic etc.
holder.addFlags(ViewHolder.FLAG_INVALID);
if (holder.isScrap()) {
removeDetachedView(holder.itemView, false);
holder.unScrap();
} else if (holder.wasReturnedFromScrap()) {
holder.clearReturnedFromScrapFlag();
}
//回收这个holder 放到mCachedViews或者mRecyclerPool中
recycleViewHolderInternal(holder);
}
holder = null;
} else {
fromScrapOrHiddenOrCache = true;
}
}
}
if (holder == null) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
+ "position " + position + "(offset:" + offsetPosition + ")."
+ "state:" + mState.getItemCount() + exceptionLabel());
}

final int type = mAdapter.getItemViewType(offsetPosition);
// 2)通过id精确的从mAttachedScrap中查找
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
if (holder != null) {
// update position
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
}
}
//如果我们自定义了缓存策略
if (holder == null && mViewCacheExtension != null) {
// We are NOT sending the offsetPosition because LayoutManager does not
// know it.
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
if (holder == null) {
throw new IllegalArgumentException("getViewForPositionAndType returned"
+ " a view which does not have a ViewHolder"
+ exceptionLabel());
} else if (holder.shouldIgnore()) {
throw new IllegalArgumentException("getViewForPositionAndType returned"
+ " a view that is ignored. You must call stopIgnoring before"
+ " returning this view." + exceptionLabel());
}
}
}
if (holder == null) { // fallback to pool
if (DEBUG) {
Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
+ position + ") fetching from shared pool");
}
//去mRecyclerPool中查找 根据不同的type拿到不同的holder,
//type就是我们在adapter中写的getItemViewType
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
if (holder == null) {
long start = getNanoTime();
if (deadlineNs != FOREVER_NS
&& !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
// abort - we have a deadline we can't meet
return null;
}
//缓存中找不到调用createViewHolder创建
holder = mAdapter.createViewHolder(RecyclerView.this, type);
if (ALLOW_THREAD_GAP_WORK) {
// only bother finding nested RV if prefetching
RecyclerView innerView = findNestedRecyclerView(holder.itemView);
if (innerView != null) {
holder.mNestedRecyclerView = new WeakReference<>(innerView);
}
}

long end = getNanoTime();
mRecyclerPool.factorInCreateTime(type, end - start);
if (DEBUG) {
Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder");
}
}
}

// This is very ugly but the only place we can grab this information
// before the View is rebound and returned to the LayoutManager for post layout ops.
// We don't need this in pre-layout since the VH is not updated by the LM.
if (fromScrapOrHiddenOrCache && !mState.isPreLayout() && holder
.hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) {
holder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
if (mState.mRunSimpleAnimations) {
int changeFlags = ItemAnimator
.buildAdapterChangeFlagsForAnimations(holder);
changeFlags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
final ItemHolderInfo info = mItemAnimator.recordPreLayoutInformation(mState,
holder, changeFlags, holder.getUnmodifiedPayloads());
recordAnimationInfoIfBouncedHiddenView(holder, info);
}
}

boolean bound = false;
if (mState.isPreLayout() && holder.isBound()) {
// do not update unless we absolutely have to.
holder.mPreLayoutPosition = position;
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {//如果没有绑定数据
if (DEBUG && holder.isRemoved()) {
throw new IllegalStateException("Removed holder should be bound and it should"
+ " come here only in pre-layout. Holder: " + holder
+ exceptionLabel());
}
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
//调用bindViewHolder
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
//给holder中的itemView设置LayoutParams
final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
final LayoutParams rvLayoutParams;
if (lp == null) {
rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
holder.itemView.setLayoutParams(rvLayoutParams);
} else if (!checkLayoutParams(lp)) {
rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
holder.itemView.setLayoutParams(rvLayoutParams);
} else {
rvLayoutParams = (LayoutParams) lp;
}
rvLayoutParams.mViewHolder = holder;
rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
return holder;
}

从上面源码可以总结一下的流程,先去mAttachedScrap中找。是要是看看View是不是刚刚剥离的,如果是直接返回如果不是,去mCachedViews中查找,mCachedViews中是精确查找,如果找到返回,找不到或者匹配不上就去mRecyclerPool中查找,找到了返回一个全新的ViewHolder,找不到的话只能调用onCreateViewHolder新建一个了。

mAttachedScrap和mCachedViews都是精确查找,找到的ViewHolder都是已经绑定好数据的,不会再调用onBindViewHolder重新绑定数据,mRecyclerPool中的ViewHolder都是清理干净的空白的ViewHolder,找到之后需要调用onBindViewHolder重新绑定数据,这点我们可以从上面代码中的第二步那跟进去看看getScrapOrCachedViewForId方法

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
ViewHolder getScrapOrCachedViewForId(long id, int type, boolean dryRun) {
// Look in our attached views first
final int count = mAttachedScrap.size();
for (int i = count - 1; i >= 0; i--) {
final ViewHolder holder = mAttachedScrap.get(i);
//判断id是否一致 是不是从Scrap中返回
//是才返回,不是去mCachedViews中找
if (holder.getItemId() == id && !holder.wasReturnedFromScrap()) {
if (type == holder.getItemViewType()) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
if (holder.isRemoved()) {
if (!mState.isPreLayout()) {
holder.setFlags(ViewHolder.FLAG_UPDATE, ViewHolder.FLAG_UPDATE
| ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED);
}
}
return holder;
} else if (!dryRun) {
mAttachedScrap.remove(i);
removeDetachedView(holder.itemView, false);
quickRecycleScrapView(holder.itemView);
}
}
}

// Search the first-level cache
final int cacheSize = mCachedViews.size();
for (int i = cacheSize - 1; i >= 0; i--) {
final ViewHolder holder = mCachedViews.get(i);
//判断id是否一致,是才返回不是放入mRecyclerPool中并从mCachedViews中移除
if (holder.getItemId() == id) {
if (type == holder.getItemViewType()) {
if (!dryRun) {
mCachedViews.remove(i);
}
return holder;
} else if (!dryRun) {
recycleCachedViewAt(i);
return null;
}
}
}
return null;
}

可以看到上面的代码中,从对应的缓存中找到holder之后,都会判断一下是不是想要的那个holder,是的话才会返回。

那RecycleView到底是怎么复用的呢?入口很多比如通过Recycler中的recycleView方法(recycler.recycleView)进去看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void recycleView(View view) {
// This public recycle method tries to make view recycle-able since layout manager
// intended to recycle this view (e.g. even if it is in scrap or change cache)
ViewHolder holder = getChildViewHolderInt(view);
if (holder.isTmpDetached()) {
removeDetachedView(view, false);
}
if (holder.isScrap()) {
holder.unScrap();
} else if (holder.wasReturnedFromScrap()) {
holder.clearReturnedFromScrapFlag();
}
recycleViewHolderInternal(holder);
}

这个方法用于回收分离的视图和把指定的视图放到缓存池中用于重新绑定和复用。最后调用了recycleViewHolderInternal方法,recycleViewHolderInternal这个方法时最终的回收方法,有的入口直接调用了这个方法

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
void recycleViewHolderInternal(ViewHolder holder) {

......

boolean cached = false;
boolean recycled = false;
if (DEBUG && mCachedViews.contains(holder)) {
throw new IllegalArgumentException("cached view received recycle internal? "
+ holder + exceptionLabel());
}
if (forceRecycle || holder.isRecyclable()) {
if (mViewCacheMax > 0
&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_REMOVED
| ViewHolder.FLAG_UPDATE
| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
// Retire oldest cached view
int cachedViewSize = mCachedViews.size();
//mViewCacheMax的值是2,所以mCachedViews中最多缓存两条数据
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
//根据先进先出原则,把最老的从mCachedViews中放到mRecyclerPool中
recycleCachedViewAt(0);
cachedViewSize--;
}

int targetCacheIndex = cachedViewSize;
if (ALLOW_THREAD_GAP_WORK
&& cachedViewSize > 0
&& !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
// when adding the view, skip past most recently prefetched views
int cacheIndex = cachedViewSize - 1;
while (cacheIndex >= 0) {
int cachedPos = mCachedViews.get(cacheIndex).mPosition;
if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
break;
}
cacheIndex--;
}
targetCacheIndex = cacheIndex + 1;
}
//将最近刚刚回收的ViewHolder放在mCachedViews里
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
//如果不设置往mCachedViews中放,就放入mRecyclerPool
if (!cached) {
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
} else {
// NOTE: A view can fail to be recycled when it is scrolled off while an animation
// runs. In this case, the item is eventually recycled by
// ItemAnimatorRestoreListener#onAnimationFinished.

// TODO: consider cancelling an animation when an item is removed scrollBy,
// to return it to the pool faster
if (DEBUG) {
Log.d(TAG, "trying to recycle a non-recycleable holder. Hopefully, it will "
+ "re-visit here. We are still removing it from animation lists"
+ exceptionLabel());
}
}
// even if the holder is not removed, we still call this method so that it is removed
// from view holder lists.
mViewInfoStore.removeViewHolder(holder);
if (!cached && !recycled && transientStatePreventsRecycling) {
holder.mOwnerRecyclerView = null;
}
}

从这里面我们看到了熟悉的mCachedViews和mRecyclerPool,这也说明了RecycleView的回收机制跟mAttachedScrap是没有关系的。

那这个回收到底是从哪里调用的呢?第一个地方就是在LayoutManager的onLayoutChildren方法中调用的detachAndScrapAttachedViews(recycler);,另一个就是Recyclerview滑动的时候调用removeAndRecycleView方法。

detachAndScrapAttachedViews仅用于布局之前,将所有的子view剥离,放在mAttachedScrap中供后面重新布局的时候使用。

removeAndRecycleView在滚动的时候,把ViewHolder标记为removed,先缓存在mCachedViews中,mCachedViews的最大容量为2,如果mCachedViews中存满了,把最先缓存进来的拿出来放到mRecyclerPool,mRecyclerPool中默认缓存5个。然后把最新的放入mCachedViews中缓存。

OK,结束。

参考:

https://www.cnblogs.com/dasusu/p/7746946.html

https://www.jianshu.com/p/504e87089589

https://blog.csdn.net/harvic880925/article/details/84866486

# 进阶

コメント

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×