RecyclerView Adapter 数据刷新应该分为两种,一种叫 item changes,这种是指某个 item 的数据内容变化,但数据列表总体的个数,顺序都不变。另一种叫 structural changes,这种变化就要涉及到数据列表个数的增,删,位置变动了。既然区分了这两种,那对应的刷新操作应该有所不同的,平时我们一概而论都用 notifyDataSetChanged 去做,虽然能达到效果,但应该还不是最佳。这次就来看看适配器刷新操作上系统提供了哪些高效方法。
//这个刷新操作没有指明数据列表是什么样的变化,强制让观察者们认为当前的这些数据都无效了
//这样一来,LayoutManager 就会被迫营业,又重新全部重新布局,重新关联数据
//就是来一遍全局洗牌,即使仅仅变了一个文本文案,这么来看,这就有点杀鸡用牛刀的感觉。
notifyDataSetChanged();
//这个刷新操作就是一个 item changes,仅更新指定位置的数据
//往下细看下实现
public final void notifyItemChanged(int position) {
mObservable.notifyItemRangeChanged(position, 1);
}
public void notifyItemRangeChanged(int positionStart, int itemCount) {
notifyItemRangeChanged(positionStart, itemCount, null);
}
public void notifyItemRangeChanged(int positionStart, int itemCount, Object payload) {
for (int i = mObservers.size() - 1; i >= 0; i--) {
mObservers.get(i).onItemRangeChanged(positionStart, itemCount, payload);
}
}
//RecyclerViewDataObserver
public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
//我估计重点是在这,这里能标记仅更新 positionStart 的位置的数据
if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)) {
//这个方法里简单看了下,最后应该还是调 requestLayout() 进行的更新
triggerUpdateProcessor();
}
}
//AdapterHelper
boolean onItemRangeChanged(int positionStart, int itemCount, Object payload) {
//mPendingUpdates 是个数组,里面存放的是 UpdateOp 类型的对象
//这个 UpdateOp 标记了数据更新类型,例如更新,新增,删除等。
//这两个的概念有点像 Message 和 MessageQueue
//我们先记住,看后面重新测量布局时会不会用到
mPendingUpdates.add(obtainUpdateOp(UpdateOp.UPDATE, positionStart, itemCount, payload));
mExistingUpdateTypes |= UpdateOp.UPDATE;
return mPendingUpdates.size() == 1;
}
我们重新从 RecyclerView 的 onMeasure() 看起,发现相关的逻辑在 dispatchLayoutStep2() 方法里,
private void dispatchLayoutStep2() {
//这个是 AdapterHelper 对象的操作,盲猜可能和更新相关
//进去看看
mAdapterHelper.consumeUpdatesInOnePass();
mState.mItemCount = mAdapter.getItemCount();
mLayout.onLayoutChildren(mRecycler, mState);
}
//AdapterHelper
void consumeUpdatesInOnePass() {
final int count = mPendingUpdates.size();
for (int i = 0; i < count; i++) {
UpdateOp op = mPendingUpdates.get(i);
switch (op.cmd) {
case UpdateOp.UPDATE:
//按只看重点的思路,其他代码省略
//onDispatchSecondPass() 最终会调用 LayoutManager 的方法
//而 LinearLayoutManager 里并没有重写 onItemsUpdated()
//所以这句最终会是个空实现。
mCallback.onDispatchSecondPass(op);
//同理,这个方法最终调用了 RecyclerView 的 viewRanageUpdate() 方法
mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload);
break;
}
}
}
//RecyclerView
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);
if (holder.mPosition >= positionStart && holder.mPosition < positionEnd) {
//标记更新,看来盲猜对了
holder.addFlags(ViewHolder.FLAG_UPDATE);
holder.addChangePayload(payload);
((LayoutParams) child.getLayoutParams()).mInsetsDirty = true;
}
}
//如果这个位置的 viewholder 也在缓存里,那么也要标记更新
mRecycler.viewRangeUpdate(positionStart, itemCount);
}
回过头继续看 dispatchLayoutStep2 的话,LayoutManager 就要开始 item view 的布局了,为了简化,我们直接略过很多层,具体的路径是在 LinearLayoutManager 的 onLayoutChildren() 方法里找到 fill(),进入之后再找到 layoutChunk(),进入之后再找 LayoutState 对象的 next(),接下去代码就比较简单,最后需要定位到 Recycler 的 tryGetViewHolderForPositionByDeadline() 方法,
//这个方法返回的是 ViewHolder 对象,按照我们的思路,因为最初调用的是 notifyItemChanged(),所以理论上来说这个 ViewHolder 对象应该可以来自缓存
ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
boolean fromScrapOrHiddenOrCache = false;
if (holder == null) {
//这步就是从 缓存 里去取,按我们的思路不出意外肯定可以取到
//里面具体细节先不去深究
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if (holder != null) {
//校验有效性
if (!validateViewHolderForOffsetPosition(holder)) {
//省略...
} else {
fromScrapOrHiddenOrCache = true;
}
}
}
if (holder == null) {
//如果上面缓存里没有取到,那在这里还会通过各种方式去获取
//最后的方案就是新建一个 ViewHolder 对象,以确保肯定有值
}
boolean bound = false;
if (mState.isPreLayout() && holder.isBound()) {
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
//不考虑第一个 isBound() 判断,之前看第二个 needsUpdate()
//之前不是添加了 ViewHolder.FLAG_UPDATE 标记么,在这里这个方法其实就是判断有么有这个标记
//那这么一来这个 position 位置的 holder 就会去重新绑定数据,
//也就是最终回调自定义 Adapter 的 onBindViewHolder() 方法
//而对于其他数据对应的 ViewHolder,因为没有标记更新,就不会再重新绑定数据
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
}
//经过代码调试,确实也是这么一个逻辑,只重新绑定有变更的数据项。
这么看来,仅是数据内容的变动,使用 notifyItemChanged() 比使用 norifyDataSetChanged() 会高效很多,特别是数据多,布局复杂的时候。
//再来看看 structrural change 类型的刷新操作,
//在 position 位置插入一个数据项,原本位置就会依次往后移动
public final void notifyItemInserted(int position) {
mObservable.notifyItemRangeInserted(position, 1);
}
//有了上面的基础,我们这里跳着看
public void onItemRangeInserted(int positionStart, int itemCount) {
if (mAdapterHelper.onItemRangeInserted(positionStart, itemCount)) {
triggerUpdateProcessor();
}
}
boolean onItemRangeInserted(int positionStart, int itemCount) {
//这里添加新增的状态
mPendingUpdates.add(obtainUpdateOp(UpdateOp.ADD, positionStart, itemCount, null));
mExistingUpdateTypes |= UpdateOp.ADD;
return mPendingUpdates.size() == 1;
}
void consumeUpdatesInOnePass() {
final int count = mPendingUpdates.size();
for (int i = 0; i < count; i++) {
UpdateOp op = mPendingUpdates.get(i);
switch (op.cmd) {
case UpdateOp.ADD:
//这个方法最终调用了 LayoutManager 的 onItemsAdded() 但是个空实现
mCallback.onDispatchSecondPass(op);
//这个方法最终会调用 RecyclerView 的 offsetPositionRecordsForInsert()
mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount);
break;
}
}
}
void offsetPositionRecordsForInsert(int positionStart, int itemCount) {
final int childCount = mChildHelper.getUnfilteredChildCount();
for (int i = 0; i < childCount; i++) {
final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
if (holder != null && !holder.shouldIgnore() && holder.mPosition >= positionStart) {
//不同于更新操作,这里看去会对 holder 进行位移操作,在插入位置及之后的 holder 都会进行调整
holder.offsetPosition(itemCount, false);
mState.mStructureChanged = true;
}
}
//缓存里对应的也要做位移操作
mRecycler.offsetPositionRecordsForInsert(positionStart, itemCount);
//做了位移就要重新做测量,布局
//但因为当前逻辑就在测量,布局过程,所以这里发起的布局申请应该要在这次完成之后开始。
requestLayout();
}
//在 tryGetViewHolderForPositionByDeadline() 方法里,经过代码调试发现,第一次的遍历执行没有做数据绑定,且 ViewHolder 对象也都来自缓存,数据列表的个数还是新增之前的。
//而在后面一次遍历执行,在指定位置上,ViewHolder 对象会通过 onCreateViewHolder() 创建,且会因为没有绑定过数据而调用 onBindViewHolder(),而除了新增的这一项,其他 ViewHolder 依然来自缓存,且不需要再进行数据绑定。
这么看来,如果是新增数据项,使用 notifyItemInserted() 依然要比使用 norifyDataSetChanged() 高效,而新增数据项对于分页操作的加载更多是很符合的。
//这也是一个 structural change 刷新操作,删除指定位置的数据项,后面的数据项依次往前移动
//相比新增,移除刷新操作也有两次 测量,布局 工作,第一次是移除之前,第二次是移除之后。
//不过两次操作下都不会做数据项的重新绑定,并且 ViewHolder 也都来自缓存
//也就是说,移除操作仅仅移除了数据,而没有新增或者重新绑定之类的操作。
//可见移除操作开销更小,效率更高
notifyItemRemoved(int position);
//虽然数据列表整体数量没有改变,但对于指定位置的数据位置有调整,所以也是 structural change 刷新操作
//其实这个内部和新增时的 ViewHolder 对象做位移操作是一样的,只是这里是相互调整,而新增时调整的方向是一致的
//同样这里也会有两次 测量,布局 工作,第一次是移动之前,第二次是移动之后。
//而两次工作都不会做 ViewHolder 对象的创建和数据绑定,因为之前已经绑定好了。
//需要注意的是,这仅仅只是某个数据项移动位置,而不是两个位置的数据项相互对调
notifyItemMoved(int fromPosition, int toPosition);
至此数据刷新相关的操作都梳理完了,基本覆盖了我们项目开发的日常操作。再回顾一下适配器的这些方法,以后就不光只会用 notifyDataSetChanged() 一种了,在合适的场景用合适的方法才能达到最优解。
//全局更新
notifyDataSetChanged();
//内容更新
notifyItemChanged(int position);
notifyItemChanged(int position, @Nullable Object payload);
notifyItemRangeChanged(int positionStart, int itemCount);
notifyItemRangeChanged(int positionStart, int itemCount, Object payload);
//数据项新增
notifyItemInserted(int position);
notifyItemRangeInserted(int positionStart, int itemCount);
//数据项删除
notifyItemRemoved(int position);
notifyItemRangeRemoved(int positionStart, int itemCount);
//数据项换位置
notifyItemMoved(int fromPosition, int toPosition);