RecyclerView 的 notifyDataSetChan

2021-04-20  本文已影响0人  jkwen

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);
上一篇下一篇

猜你喜欢

热点阅读