Fork me on GitHub

RecycleBin/Recycler的回收机制(下)

在上篇中我们简单分析了下listview的layout过程和回收机制,那这里我们将对RecyclerView的回收机制进行一波带走,please ready!


在ListView的复用中,主要是以View为单位的,而在RecyclerView中主要是以ViewHolder为单位,其实这一点我们从各自的Adapter中就可以看出:
ListView:主要重写createViewFromResource()和getView()方法。
RecyclerView:主要重写onCreateViewHolder()和onBindViewHolder()方法。而且RecyclerView已经帮我们实现了view的复用。
注:这里的ViewHolder也不像ListView中一样仅仅保存一个view,RecyclerView中的ViewHolder不仅仅包含view,还包括view的当前position、以前的position、itemId、itemViewType、当前的状态信息等等,这些丰富的信息可以把每一个view的状态区分开来,为我们RecyclerView实现局部刷新的功能打下了基础。


Recycler

我们知道,RecyclerView的回收复用主要靠Recycler来实现。首先,RecyclerView直接继承自ViewGroup,而Recycler则是RecyclerView中的final类,那么来看看这个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* A Recycler is responsible for managing scrapped or detached item views for reuse.
*/
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;
.....
}

官方解释:Recycler是用来管理废弃view的复用。
可以看到Recycler的属性中有mAttachedScrap、mChangedScrap、mCachedViews、mRecyclerPool、mViewCacheExtension这几个管理View的集合,这也对应了Recycler中的四级缓存:

  • scrap view:mAttachedScrap、mChangedScrap。官方对scrapview的解释是:已标记为要删除或重复使用但仍然与其父RecyclerView绑定的视图。在这一级,mAttachedScrap是用来缓存屏幕上的viewholder(主要在recyclerview的layout过程中的进行),而mChangedScrap则回收屏幕上数据有变化的viewholder。

  • mCachedViews:缓存屏幕外的ViewHolder,默认为2个。

  • mViewCacheExtension:需要用户定制,默认不实现。ViewCacheExtension是一个抽象类,帮助开发者缓存view、自定义缓存控制策略。

  • mRecyclerPool:缓存池,多个RecyclerView共用,但存在这里的 ViewHolder 的数据信息会被重置掉,所以需要重新调用 onBindViewHolder 来绑定数据。每种type的pool容量为5。每种type的底层是一个ArrayList,然后所有的ArrayList又放在map(sparseArray)中


复用

Recycler类的入口(被调用最多的)是 getViewForPosition()方法,也是复用的核心入口。方法返回我们想要的view(复用的or重新创建的)。

1
2
3
4
5
6
7
public View getViewForPosition(int position) {
return getViewForPosition(position, false);
}
View getViewForPosition(int position, boolean dryRun) {
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
/**
* Attempts to get the ViewHolder for the given position, either from the Recycler scrap,
* cache, the RecycledViewPool, or creating it directly.
*/
@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs)
{
ViewHolder holder = null;
// If there is a changed scrap, try to find from there
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
// 1) Find by position from scrap/hidden list/cache
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
...
}
}
/**
* Returns a view for the position either from attach scrap, hidden children, or cache.
*/
ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
// Try first for an exact, non-invalid match from scrap.
for (int i = 0; i < scrapCount; i++) {
final ViewHolder holder = mAttachedScrap.get(i);
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
&& !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved()))
{
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
}

可以看到首先会去mChangedScrap寻找我们想要的view,如果没有再去调用getScrapOrHiddenOrCachedHolderForPosition()方法去scarpview中寻找,可以看出,先遍历mAttachedScrap集合寻找对应的viewholder。其实在上面已经提到了mAttachedScrap主要是在recyclerview重新layout的时候回收当前在屏幕上显示的viewholder,便于下次attach,这一点是和listview的activeViews类似的,它们的生命周期都是layout过程。

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
ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
.....//省略上面的代码
if (!dryRun) {
View view = mChildHelper.findHiddenNonRemovedView(position);
if (view != null) {
// This View is good to be used. We just need to unhide, detach and move to the
// scrap list.
final ViewHolder vh = getChildViewHolderInt(view);
mChildHelper.unhide(view);
int layoutIndex = mChildHelper.indexOfChild(view);
if (layoutIndex == RecyclerView.NO_POSITION) {
throw new IllegalStateException("layout index should not be -1 after "
+ "unhiding a view:" + vh);
}
mChildHelper.detachViewFromParent(layoutIndex);
scrapView(view);
vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP|ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
return vh;
}
}
// Search in our first-level recycled view cache.
final int cacheSize = mCachedViews.size();
for (int i = 0; i < cacheSize; i++) {
final ViewHolder holder = mCachedViews.get(i);
// invalid view holders may be in cache if adapter has stable ids as they can be
// retrieved via getScrapOrCachedViewForId
if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
if (!dryRun) {
mCachedViews.remove(i);
}
if (DEBUG) {
Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position
+ ") found match in cache: " + holder);
}
return holder;
}
}
}

继续看下一步,这dryRun值是从getViewForPosition这里传入的false,官方解释为如果为true代表viewholder不能从scrap或cache中移除。emmm…..不懂,先跳过,反正这里为false,继续往下。进入if,去mHiddenViews寻找合适的view,这里的mHiddenViews主要是存储当前隐藏起来的view,其实我们平时常用的滑动复用用不到这一块,所以跳过继续向下走。接下来就会去mCachedViews中寻找,如果没有则返回null。
其实这一块已经将从scrap或cache中的复用走了一遍,那我们回到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
ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) {
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if (holder != null) {
if (!validateViewHolderForOffsetPosition(holder)) {
// recycle holder (and unscrap if relevant) since it can't be used
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();
}
recycleViewHolderInternal(holder);
}
holder = null;
} else {
fromScrapOrHiddenOrCache = true;
}
}
}
}

如果找到viewholder的话,判断viewholder是否符合条件,比如是否已经被remove、type and id是否匹配,如果不符合条件返回null。

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
if (holder == null) {
final int type = mAdapter.getItemViewType(offsetPosition);
// 2) Find from scrap/cache via stable ids, if exists
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");
} else if (holder.shouldIgnore()) {
throw new IllegalArgumentException("getViewForPositionAndType returned"
+ " a view that is ignored. You must call stopIgnoring before"
+ " returning this view.");
}
}
}
}

继续向下找,这一块有两部分,第一部分还是从scrap和cache view中寻找,只不过是根据id来找,逻辑和上面分析的通过position查找类似;第二部分是从mViewCacheExtension中寻找,这部分缓存是由我们自定义的,默认为null,所以跳过。继续向下走

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if (holder == null) {
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
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;
}

下一步就到了pool,这一步就是从mRecyclerPool中根据type寻找viewholder,可以看到首先判断pool中是否缓存有该type的viewholder,若无返回null,若有则返回pool中最后一个viewholder缓存。拿出来就可以直接用,但是这里通过resetInternal()重置了数据,变成一张白纸,需要等待重新绑定数据。

1
2
3
4
5
6
if (holder == null) {
...
holder = mAdapter.createViewHolder(RecyclerView.this, type);
...
mRecyclerPool.factorInCreateTime(type, end - start);
}

继续向下走,如果holder还为null,则调用mAdapter.createViewHolder(),重新创建新的viewholder。并且在这一步会去mRecyclerPool创建对应type的ScrapData。

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
//从scrap和cache中获取的则不需要rebind
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);
}
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
//设置布局参数
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;

tryGetViewHolderForPositionByDeadline()的这点尾巴主要是对viewholder重新绑定数据、检查和设置布局参数。可以看到在rebind的判定条件里,像holder.isBound()这样的,从源码中可以看出(这里就不截取了),都是对mFlags进行不为0的判断,但是像上面提到的重置viewholder后,就会将mFlags设为0,所以就进入if语句中调用tryBindViewHolderByDeadline()方法,从而继续去调用mAdapter.bindViewHolder()。最后返回一个我们需要的holder。


到这里,recycler的复用机制已经大概明了了,可以大致理解成下图:

图中的每一步都是需要判断条件的,所以在不同的场景下可能会有不同的“解法”,每一步都有可能输出对应的viewholder,而且在上述的分析过程也省略了一些情况,像notifyDataSetChanged()或者重新setAdapter这些场景,都会触发不同的缓存策略,这里只是笼统的介绍了大致流程,遇到具体需求还得具体分析。


回收

上面我们简单分析了下怎么拿,下面我们看看怎么放,因为回收的入口比较多,所以我们就挑个常见的滑动场景进行分析。
滑动recyclerview的时候,由layoutmanager的removeAndRecycleView()进行回收,最后会调用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);
}

首先判断view是否是attached状态,如果是,则需要remove并且进行detached,我们在上面分析过,attached的view会存放在mAttachedScrap缓存中,这里面的view是不需要rebind的,所以这里对view解绑并且从scrap中移除。后面主要就是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
70
71
72
73
74
75
76
77
78
79
80
81
82
void recycleViewHolderInternal(ViewHolder holder) {
....
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();
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
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添加到cacheview中
mCachedViews.add(targetCacheIndex, holder);//核心
cached = true;
}
//核心
if (!cached) {
//添加到pool中
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
}
...
}
//下面的方法都是延伸展示的方法,方便理解
void recycleCachedViewAt(int cachedViewIndex) {
......
addViewHolderToRecycledViewPool(viewHolder, true);
mCachedViews.remove(cachedViewIndex);
}
void addViewHolderToRecycledViewPool(ViewHolder holder, boolean dispatchRecycled) {
clearNestedRecyclerViewIfNotNested(holder);
ViewCompat.setAccessibilityDelegate(holder.itemView, null);
if (dispatchRecycled) {
//回调adapter中的onViewRecycled方法
dispatchViewRecycled(holder);
}
holder.mOwnerRecyclerView = null;
getRecycledViewPool().putRecycledView(holder);
}
public void putRecycledView(ViewHolder scrap) {
final int viewType = scrap.getItemViewType();
//从map中获取对应type的ArrayList
final ArrayList 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);
}
private ScrapData getScrapDataForType(int viewType) {
ScrapData scrapData = mScrap.get(viewType);
if (scrapData == null) {
scrapData = new ScrapData();
mScrap.put(viewType, scrapData);
}
return scrapData;
}

recycleViewHolderInternal()里面的逻辑主要是对mCachedViews和pool的操作。这里会保证mCachedViews不超过默认大小2(mViewCacheMax),一旦大于或等与这个值,就会将mcachedViews中的0号位置的viewholder放入pool中,如果不存在这种type的pool,则会去创建新的scrapData,所以一般来说在滑动recyclerview的过程中,一开始除了会创建所能看见的一屏的item数量的viewholder,后面如果继续滑动还会创建3个新的包括cacheviews中的2个和pool中的1个,之后的滑动才会对这些viewholder进行复用(这里假定一行只有一个item)。
其实,mcachedViews中存储的一直都是要回收的holder,而且因为mcachedViews中holder复用的条件较高,需要是原来位置的item才可以获取到,所以pool中的holder才是我们要复用调用的holder。


小结

到这里,recyclerview的回收复用机制就基本过了一遍,笔记有点长,大家可以分开来看。其实recycler有些地方与listview比较像,像layout过程中scrapview的思想,但是也可以看出来RecyclerView更加注重view的回收复用,它将view的layout等操作都交给了layoutmanager,这些等后面有时间了再分析一波。