深入 RecyclerView 源码探究四:回收复用和动画

文章目录
  1. 1. 回收复用
  2. 2. 动画
  3. 3. 参考文献

只要有初恋般的热情和宗教般的意志方能成就某种事业。

前面几期分析都谈到过 Recycler,其作用就是重用 itemView。本篇在深入 RecyclerView 源码探究三:绘制和滑动的基础上继续展开,分析 RecyclerView 的回收复用和动画过程。

回收复用

RecyclerView 起着重用 itemView 的作用:填充 itemView 时,从其获取 itemView;滑出屏幕的 itemView 由其回收。不同状态的 itemView 存储在不同的集合中,如 scrapped、cached、exCached 和 recycled 等。

之前提到的 layoutChunk() 方法中,有 layoutState.next(recycler),其作用即是获取 itemView,进入里面会看到调用到 Recyler.getViewForPosition() 方法,源码如下:

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
View getViewForPosition(int position, boolean dryRun) {
// ...
// 0) If there is a changed scrap, try to find from there
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrap = holder != null;
}
// 1) Find from scrap by position
if (holder == null) {
holder = getScrapViewForPosition(position, INVALID_TYPE, dryRun);
// ...
}
}
if (holder == null) {
// ...
final int type = mAdapter.getItemViewType(offsetPosition);
// 2) Find from scrap via stable ids, if exists
if (mAdapter.hasStableIds()) {
holder = getScrapViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
// ...
}
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 (holder == null) { // fallback to recycler
// try recycler.
// Head to the shared pool.
// ...
holder = getRecycledViewPool().getRecycledView(type);
// ...
}
if (holder == null) {
holder = mAdapter.createViewHolder(RecyclerView.this, type);
// ...
}
}

// 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 (fromScrap && !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()) {
// ...
mAdapter.bindViewHolder(holder, offsetPosition);
attachAccessibilityDelegate(holder.itemView);
// ...
}
// ...
return holder.itemView;
}

上述代码中,由列表位置获取 itemView,先后分别从 scrapped、cached、exCached 和 recycled 集合中查找到相应的 itemView,若未找到,则创建 (有这一行,mAdapter.createViewHolder()),最后与数据集绑定。其中,scrapped、cached 和 exCached 集合定义在 RecyclerView.Recycler 中,分别表示将要在 RecyclerView 中删除的一级缓存 itemView、二级缓存 itemView 和三级缓存 itemView。注意,cached 集合的大小默认为 2,exCached 集合默认没有,是通过 RecyclerView.ViewCacheExtension 实现的,而 recycled 集合是一个 Map,定义在 RecyclerView.RecycledViewPool 中,将 itemView 以 itemType 分类保存下来。实际上,不同的 RecyclerView 通过设置同一个 RecyclerView.RecycledViewPool 实现其之间的共享 itemView。下面来详细分析下:

  • 一级缓存

Recycler 类里的一系列 mCachedViews。若仍依赖于 RecyclerView (比如已滑出可视范围,但还未被移除),却已经被标记上移除的 itemView 集合会被添加到 mAttachedScrap 中。接着,若 mAttachedScrap 中不再依赖时,则会被加入到 mCachedViews 中。其中,mChangedScrap 是存储 notifyXXX 方法时需要改变的 ViewHolder。

  • 二级缓存

ViewCacheExtention 是一个抽象静态类,充当附加的缓存池。若 RecyclerView 从一级缓存中找不到需要的 View 时,将会从 ViewCacheExtention 中找。注意,该缓存由开发者自行维护,若未设置,则不会启用。因为系统已经预先提供了两级缓存,所以通常也不会去设置它。除非有特殊需求,如在调用系统的缓存池之前,返回一个特定的视图,这时,该二级缓存派上用场。

  • 三级缓存

该缓存十分强大。我们知道,与 ListView 直接缓存 itemView 不同,RecyclerView 缓存的是 ViewHolder,写 Adapter 时必须要继承一个固定的 ViewHolder。首先看 RecycledViewPool 的部分节选源码如下:

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
public static class RecycledViewPool {
// 由 viewType 保存的被废弃的 ViewHolder 集合,方便下次使用
private SparseArray<ArrayList<ViewHolder>> mScrap =
new SparseArray<ArrayList<ViewHolder>>();

// ...
public void setMaxRecycledViews(int viewType, int max) {
mMaxScrap.put(viewType, max);
final ArrayList<ViewHolder> scrapHeap = mScrap.get(viewType);
if (scrapHeap != null) {
while (scrapHeap.size() > max) {
scrapHeap.remove(scrapHeap.size() - 1);
}
}
}

/**
* 缓存池移除并返回一个 ViewHolder
*/

public ViewHolder getRecycledView(int viewType) {
final ArrayList<ViewHolder> scrapHeap = mScrap.get(viewType);
if (scrapHeap != null && !scrapHeap.isEmpty()) {
final int index = scrapHeap.size() - 1;
final ViewHolder scrap = scrapHeap.get(index);
scrapHeap.remove(index);
return scrap;
}
return null;
}

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

/**
* 分离旧的 adapter,依附新的 adapter
*/

void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter,
boolean compatibleWithPrevious)
{

if (oldAdapter != null) {
detach();
}
if (!compatibleWithPrevious && mAttachCount == 0) {
clear();
}
if (newAdapter != null) {
attach(newAdapter);
}
}

/**
* 由 viewType 获取对应的缓存池
*/

private ArrayList<ViewHolder> getScrapHeapForType(int viewType) {
ArrayList<ViewHolder> scrap = mScrap.get(viewType);
if (scrap == null) {
scrap = new ArrayList<>();
mScrap.put(viewType, scrap);
if (mMaxScrap.indexOfKey(viewType) < 0) {
mMaxScrap.put(viewType, DEFAULT_MAX_SCRAP);
}
}
return scrap;
}
}

很明显,该 RecyledViewPool 是一个缓存池,是通过一个默认大小为 5 的 ArrayList 实现的,和 ListView 的 RecyclerBin 一样。然后。每一个 ArrayList 是放在一个 Map 里的,SparseArray 其实就是两个数组,用来替代 Map 的。如此设计,根据 itemType 来取用不同的缓存 Holder,每一个 Holder 有其对应的缓存,仅仅只需要为这些不同的 RecyclerView 设置同一个 Pool。

好,知道了 itemView 从不同的集合中获取的方式,再来看看 RecyclerView 何时在向这些集合中添加 itemView。

scrapped 集合中存储的是正在执行 remove 操作的 itemView。

fill() 方法的循环体中有 recyclerByLayoutState(recycler, layoutState),最终走到 recycleViewHolderInternal() 方法,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void recycleViewHolderInternal(ViewHolder holder) {
// ...
if (forceRecycle || holder.isRecyclable()) {
if (!holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED
| ViewHolder.FLAG_UPDATE)) {
// Retire oldest cached view
final int cachedViewSize = mCachedViews.size();
if (cachedViewSize == mViewCacheMax && cachedViewSize > 0) {
recycleCachedViewAt(0);
}
if (cachedViewSize < mViewCacheMax) {
mCachedViews.add(holder);
cached = true;
}
}
if (!cached) {
addViewHolderToRecycledViewPool(holder);
recycled = true;
}
}
// ...
}

首先判断集合 cached 是否满了,若满了,则从 cached 集合中移出一个到 recycled 集合中,再将新的 itemView 添加到 cached 集合中;否则就直接将 itemView 添加到 cached 集合中。

最后,exCached 集合由我们自己创建,添加或删除元素自行实现。

动画

RecyclerView 定义了 4 种针对数据集的操作,分别为 ADD、REMOVE、UPDATE 和 MOVE,封装在 AdapterHelper.UpdateOp 类中,所有操作由一个大小为 30 的对象池管理。当对数据集进行任何操作时,都会从该对象池中取出一个 UpdateOp 对象,放入等待队列中。最后,调用 RecyclerViewDataObserver.triggerUpdateProcessor() 方法,再根据这个队列中的信息,对所有子控件重新测量、布局、绘制并执行动画。以上即调用 Adapter.notifyItemXXX() 系列方法执行的一系列事件。我们通常所熟知的方法如下:

1
2
3
4
5
6
7
8
9
notifyDataSetChanged()
notifyItemChanged(int position)
notifyItemRangeChanged(int positionStart, int itemCount)
notifyItemRangeChanged(int positionStart, int itemCount, Object payload)
notifyItemInserted(int position)
notifyItemMoved(int fromPosition, int toPosition)
notifyItemRangeInserted(int positionStart, int itemCount)
notifyItemRemoved(int position)
notifyItemRangeRemoved(int positionStart, int itemCount)

显然,对某个 itemView 做操作时,会影响到其他 itemView。下面以 REMOVE 为例,梳理下流程。

首先,调用 Adapter.notifyItemRemove(),走到方法 RecyclerViewDataObserver.onItemRangeRemoved() 里:

1
2
3
4
5
6
public void onItemRangeRemoved(int positionStart, int itemCount) {
assertNotInLayoutOrScroll(null);
if (mAdapterHelper.onItemRangeRemoved(positionStart, itemCount)) {
triggerUpdateProcessor();
}
}

这里,mAdapterHelper.onItemRangeRemoved() 即之前提及的等待队列中,添加一个类型为 REMOVE 的 UpdateOp 对象,triggerUpdateProcessor() 方法会调用 requestLayout() 方法,导致界面重新布局,即随后会调用 onLayout() 方法,回到之前分析过的绘制流程。动画执行的地方呢?主要来看看 dispatchLayoutStep3() 方法:

1
2
3
4
5
6
7
8
9
10
private void dispatchLayoutStep3() {
// ...
if (mState.mRunSimpleAnimations) {
// Step 3: Find out where things are now, and process change animations.
// ...
// Step 4: Process view info lists and trigger animations
mViewInfoStore.process(mViewInfoProcessCallback);
}
// ...
}

该部分只是初始化或赋值一些执行动画需要的中间数据,process() 方法最终走到 RecyclerView.animateDisappearance() 方法,源码如下:

1
2
3
4
5
6
7
private void animateDisappearance(...) {
addAnimatingView(holder);
holder.setIsRecyclable(false);
if (mItemAnimator.animateDisappearance(holder, preLayoutInfo, postLayoutInfo)) {
postAnimationRunner();
}
}

animateDisappearance() 方法会把一个动画与 itemView 绑定,添加到待执行队列中,postAnimationRunner() 调用后会执行该队列中的动画,方法 addAnimatingView():

1
2
3
4
5
6
private void addAnimatingView(ViewHolder viewHolder) {
final View view = viewHolder.itemView;
// ...
mChildHelper.addView(view, true);
// ...
}

向 ChildHelper 中的一个名为 mHiddenViews 的集合中添加给定的 itemView,之前提到的 getViewForPosition() 方法中有个 getScrapViewForPosition() 方法,作用为从 scrapped 集合中获取 itemView:

1
2
3
4
5
ViewHolder getScrapViewForPosition(int position, int type, boolean dryRun) {
// ...
View view = mChildHelper.findHiddenNonRemovedView(position, type);
// ...
}

看其中的 findHiddenNonRemovedView() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
View findHiddenNonRemovedView(int position, int type) {
final int count = mHiddenViews.size();
for (int i = 0; i < count; i++) {
final View view = mHiddenViews.get(i);
RecyclerView.ViewHolder holder = mCallback.getChildViewHolder(view);
if (holder.getLayoutPosition() == position && !holder.isInvalid() && !holder.isRemoved()
&& (type == RecyclerView.INVALID_TYPE || holder.getItemViewType() == type)) {
return view;
}
}
return null;
}

与之前提及的 scrapped 集合联系起来了。

关于动画,文中提到的对数据集的 4 种操作,在 DefaultItemAnimator 中给出了对应的默认实现,即改变透明度,实现淡入淡出的效果。

至此,关于 RecyclerView 的回收复用和动画过程探究完毕,整个 RecyclerView 的源码探究系列也到此结束,需要深究、拓展与实践的点还有很多。

本人才疏学浅,如有疏漏错误之处,望读者中有识之士不吝赐教,谢谢。

1
Email: [email protected] / WeChat: Wolverine623

您也可以关注我个人的微信公众号码农六哥第一时间获得博客的更新通知,或后台留言与我交流

参考文献

1.http://blog.csdn.net/qq_23012315/article/details/50807224

2.https://www.kymjs.com/code/2016/07/10/01/