美文网首页
RecyclerView 的 notifyDataSetChan

RecyclerView 的 notifyDataSetChan

作者: jkwen | 来源:发表于2021-04-20 08:14 被阅读0次

    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);
    

    相关文章

      网友评论

          本文标题:RecyclerView 的 notifyDataSetChan

          本文链接:https://www.haomeiwen.com/subject/mohplltx.html