为什么我们在有数据刷新的时候推荐大家使用 notifyItemXxxChanged 等方法而不是使用 notifyDataSetChanged 方法呢?
- 在调用 notifyDataSetChanged 方法后,所有的子 view (无论是否真的需要更新)都会被添加一个标记,这个标记导致它们最后都被缓存到 RecyclerPool 中,然后重新绑定数据。并且由于 RecyclerPool 有容量限制,如果不够最后就要重新创建新的视图了。
- 但是使用 notifyItemXxxChanged 等方法会将 ViewHolder 缓存到 mChangedScrap 和 mAttachedScrap 中,这两个缓存是没有容量限制的,所以原有的 ViewHolder 可以得到最大程度的复用,基本不需要重新创建新的 ViewHolder,只是 mChangedScrap 中的视图需要重新执行 bind。
1、RecyclerView 中的观察者模式
一般我们都是调用 Adapter#notifyXxx 方法通知数据变更,然后 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。
|
|
这两个标记(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 方法
|
|
也就是说,没有给 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 ,监听下一个垂直同步信号,进行刷新
Q:可以看到上面这个方法往队列中添加了一个 UpdateOp,在哪里取出来执行呢?
A: 会从 AdapterHelper#consumePostponedUpdates 方法中取出通知 Callback 去执行。
这个 AdapterHelper#mCallback 这个成员变量是在什么时候赋值的?可以看到AdapterHelper 构造函数接受一个 Callback 参数。当 RecyclerView 在初始化时,会调用 RecyclerView#initAdapterManager,创建一个 AdapterHelper,此时会通过创建匿名内部类的方式构造一个 Callback 实例,传递给 AdapterHelper,AdapterHelper 在消费操作序列的时候,会调用 Callback 相应的方法。
回归主线,我们调用了 notifyItemChange 方法,会触发 CallBack 的 onDispatchSecondPass 和 markViewHoldersUpdated。跟上面的 notifyDataSetChange 方法类似,这里最终也触发了一个 viewHolder 的标记行为,这个标记是通过调用 RecyclerView#viewRangeUpdate 完成的。只不过
细粒度通知的方法,也会给 ViewHolder 打上一个 flag,只是这个 flag 的值跟 notifyDataSetChange 方法通知更新时不同。
这个 flag 啥时候会派上用场呢?
细粒度通知同样会触发布局,调用到 onLayoutChildren 方法,最终也会触发前面提到的 scrapOrRecycleView
首先调用 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 后续能够得到更大程度的复用。