为什么推荐使用细粒度的通知方法更新 RecyclerView?

为什么我们在有数据刷新的时候推荐大家使用 notifyItemXxxChanged 等方法而不是使用 notifyDataSetChanged 方法呢?

  • 在调用 notifyDataSetChanged 方法后,所有的子 view (无论是否真的需要更新)都会被添加一个标记,这个标记导致它们最后都被缓存到 RecyclerPool 中,然后重新绑定数据。并且由于 RecyclerPool 有容量限制,如果不够最后就要重新创建新的视图了
  • 但是使用 notifyItemXxxChanged 等方法会将 ViewHolder 缓存到 mChangedScrap 和 mAttachedScrap 中,这两个缓存是没有容量限制的,所以原有的 ViewHolder 可以得到最大程度的复用,基本不需要重新创建新的 ViewHolder,只是 mChangedScrap 中的视图需要重新执行 bind。

1、RecyclerView 中的观察者模式

一般我们都是调用 Adapter#notifyXxx 方法通知数据变更,然后 RecyclerView 会刷新,这种机制看上去是不是有点眼熟?是的,RecyclerView 中使用到了我们常见的观察者模式。
如下时序图所示:
RecyclerView观察者模式时序图
说明

  • Adapter 中的数据是被观察者,也就是上图中的 AdapterDataObservable。
  • RecyclerViewDataObserver 是观察者,在数据集变化时做出对应的响应。RecyclerView 通过组合的方式使用到这个类。
  • 注册过程:外部调用 RecyclerView#setAdapter 方法的时候,RecyclerView 会将自身的一个 RecyclerViewDataObserver 实例注册为监听者。
  • 通知过程:外部调用 Adapter#notifyXxxChanged 方法时,会触发 AdapterDataObservable#notifyXxxChanged,最终通知到上面注册的 RecyclerViewDataObserver 对应的方法。

2、notifyDataSetChanged

以 notifyDataSetChanged 方法为例,该方法最终会调用到 RecyclerViewDataObserver#onChanged 方法,该方法会处理结构性变化 。具体实现位于 processDataSetCompletelyChange 方法中,该方法会调用RecyclerView#markKnownViewsInvalid 将 ViewHolder 标记为 invalid

1
2
3
4
5
6
7
8
9
10
11
12
13
void markKnownViewsInvalid() {
final int childCount = mChildHelper.getUnfilteredChildCount();
for (int i = 0; i < childCount; i++) {
final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
if (holder != null && !holder.shouldIgnore()) {
//将当前 RecyclerView 的所有子 ViewHolder 添加 FLAG_UPDATE 和 FLAG_INVALID 标记
holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID);
}
}
markItemDecorInsetsDirty();
//对 mCachedView 中的子 ViewHolder 添加 FLAG_UPDATE 和 FLAG_INVALID 标记
mRecycler.markKnownViewsInvalid();
}

这两个标记(FLAG_UPDATE、FLAG_INVALID)与后续的更新以及缓存有很大的关系(在后面内容会说明)

重点👆👆👆

假设此时没有 pendingUpdate(注:AdapterHelper#mPendingUpdates 中存储着外部调用 notifyXxx 方法触发的 UpdateOp,一个 UpdateOp 对应一次更新操作) ,会触发 requestLayout。 最终调用到 ViewRootImpl#requestLayout,触发 scheduleTraversals,等下一次垂直信号过来的时候,触发 performTraversal,重新进行布局。
也就是说会调用到 RecyclerView#dispatchLayout 方法,然后触发 dispatchLayoutStep1、2、3。步骤 2 会触发 LayoutManager#onLayoutChildren (开启预测动画的情况下,步骤 1 也会触发 onLayoutChildren)
以 LinearLayoutManager 为例,onLayoutChildren 方法内部会调用 detachAndScrapAttachedViews 方法

1
2
3
4
5
6
7
public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
final int childCount = getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
final View v = getChildAt(i);
scrapOrRecycleView(recycler, i, v);
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
//省略一些代码
//前面的标志位,在这用到了。
//如果为 invalid 并且没有被 removed 并且 没有stableId(通常情况下,我们都不会设置 stableId),因此,调用 notifyDatasetChange 方法,会走到👇这个 if 分支,将 ViewHolder 添加到 RecyclerViewPool 中。
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {
removeViewAt(index);
recycler.recycleViewHolderInternal(viewHolder);
} else {
detachViewAt(index);
recycler.scrapView(view);
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
}
}

也就是说,没有给 Adapter 设置 hasStableId 的情况下,notifyDataSetChange 方法触发的更新,会使 ViewHolder 被放到 RecycledViewPool 中,而默认情况下,RecycledViewPool 中每种 itemType 的 ViewHolder 缓存上限为 5 个,如果屏幕显示的ViewHolder 对应的 itemType 比较单一,并且显示的个数超过 5 个,假设屏幕中显示的相同 itemType 的 ViewHolder 个数为 n,那么将会有 n-5 个 ViewHolder 因为缓存满了,而无法放入 RecyclerViewPool,也就不能复用了,这将导致后续需要重新创建相应的 ViewHolder。

👆👆👆 划重点!

3、notifyItemXxxChange

细粒度的更新方法,整体思路,某一个/多个 item 变化了,那么通知对应的 item ,诶,老铁,数据更新了,你得重新执行一次 bindViewHolder。
再来看看 notifyItemChange,最终也会调用到 RecyclerViewDataObserver#onItemRangeChanged ,首先触发 AdapterHelper#onItemRangeChanged,往队列中添加一个 update 操作。如果 mPendingUpdates 队列中只有一个 item,那么直接调用 RecyclerView#requestLayout ,监听下一个垂直同步信号,进行刷新

1
2
3
4
5
6
7
8
9
//AdapterHelper#onItemRangeChanged
boolean onItemRangeChanged(int positionStart, int itemCount, Object payload) {
if (itemCount < 1) {
return false;
}
mPendingUpdates.add(obtainUpdateOp(UpdateOp.UPDATE, positionStart, itemCount, payload));
mExistingUpdateTypes |= UpdateOp.UPDATE;
return mPendingUpdates.size() == 1;
}

Q:可以看到上面这个方法往队列中添加了一个 UpdateOp,在哪里取出来执行呢?
A: 会从 AdapterHelper#consumePostponedUpdates 方法中取出通知 Callback 去执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void consumeUpdatesInOnePass() {
// we still consume postponed updates (if there is) in case there was a pre-process call
// w/o a matching consumePostponedUpdates.
consumePostponedUpdates();
final int count = mPendingUpdates.size();
for (int i = 0; i < count; i++) {
UpdateOp op = mPendingUpdates.get(i);
switch (op.cmd) {
//省略一些代码
case UpdateOp.UPDATE:
mCallback.onDispatchSecondPass(op);
mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload);
break;
//省略一些代码
}
if (mOnItemProcessedCallback != null) {
mOnItemProcessedCallback.run();
}
}
//省略一些代码
}

这个 AdapterHelper#mCallback 这个成员变量是在什么时候赋值的?可以看到AdapterHelper 构造函数接受一个 Callback 参数。当 RecyclerView 在初始化时,会调用 RecyclerView#initAdapterManager,创建一个 AdapterHelper,此时会通过创建匿名内部类的方式构造一个 Callback 实例,传递给 AdapterHelper,AdapterHelper 在消费操作序列的时候,会调用 Callback 相应的方法。
回归主线,我们调用了 notifyItemChange 方法,会触发 CallBack 的 onDispatchSecondPass 和 markViewHoldersUpdated。跟上面的 notifyDataSetChange 方法类似,这里最终也触发了一个 viewHolder 的标记行为,这个标记是通过调用 RecyclerView#viewRangeUpdate 完成的。只不过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// RecyclerView#viewRangeUpdate
void viewRangeUpdate(int positionStart, int itemCount, Object payload) {
final int childCount = mChildHelper.getUnfilteredChildCount();
final int positionEnd = positionStart + itemCount;
for (int i = 0; i < childCount; i++) {
final View child = mChildHelper.getUnfilteredChildAt(i);
final ViewHolder holder = getChildViewHolderInt(child);
if (holder == null || holder.shouldIgnore()) {
continue;
}
if (holder.mPosition >= positionStart && holder.mPosition < positionEnd) {
//标记为 FLAG_UPDATE
holder.addFlags(ViewHolder.FLAG_UPDATE);
//省略一些代码
}
}
// 同时也会给 RecyclerView.Recycler#mCachedViews 中的位于区间内的 ViewHolder 加上 ViewHolder.FLAG_UPDATE 标记,
mRecycler.viewRangeUpdate(positionStart, itemCount);
}

细粒度通知的方法,也会给 ViewHolder 打上一个 flag,只是这个 flag 的值跟 notifyDataSetChange 方法通知更新时不同。
这个 flag 啥时候会派上用场呢?
细粒度通知同样会触发布局,调用到 onLayoutChildren 方法,最终也会触发前面提到的 scrapOrRecycleView

1
2
3
4
5
6
7
8
9
10
11
12
13
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
//省略一些代码
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {
removeViewAt(index);
recycler.recycleViewHolderInternal(viewHolder);
} else {
//因为添加的 flag 是 FLAG_UPDATE ,会走到这个逻辑分支
detachViewAt(index);
recycler.scrapView(view);
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
}
}

首先调用 detachViewAt 方法将 ViewHolder 对应的 view 中 recyclerView 中移除,注意,跟 removeView 不同的是,这个方法只是将 view 从 parent 的 mChildren 数组中移除,将 view 的 parent 置为 null,但是并非真正的移除,因此不会触发requestLayout 和 invalidate。但是这种状态也只是暂时的,一般会很快触发 attachViewToParent 或者是 removeDetachedView。
具体看下 RecyclerView.Recycler#scrapView 方法,给 FLAG_UPDATE 的 ViewHolder 会被添加到 mChangedScrap 中,后续重新执行 bind。然后其他view,会被放到 mAttachedScrap 中。无论是 mAttachedScrap 还是 mChangedScrap ,都没有容量限制,因此,屏幕上的 ViewHolder 后续能够得到更大程度的复用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void scrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
|| !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
//省略一些代码
holder.setScrapContainer(this, false);
mAttachedScrap.add(holder);
} else {
if (mChangedScrap == null) {
mChangedScrap = new ArrayList<ViewHolder>();
}
holder.setScrapContainer(this, true);
mChangedScrap.add(holder);
}
}

Show Comments
0%