RecycleView的缓存原理
上一篇文章RecycleView的绘制流程,当我们走到子View的布局流程的layoutChunk方法的时候,通过View view = layoutState.next(recycler);
方法获取将要布局的子View,然后进行后续操作,现在来看一下这个子View是怎么获取的。1
2
3
4
5
6
7
8View 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
6public 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
18public 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 | ViewHolder tryGetViewHolderForPositionByDeadline(int position, |
从上面源码可以总结一下的流程,先去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
44ViewHolder 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
14public 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
69void 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