本文原作者: BennuC

recycleview 在中间新增数据 不刷新当前item位置 recyclerview刷新数据_开发者

前言

使用 RecyclerView 的时候会遇到这样一个场景,使用 notifyItemChanged 刷新数据的时候会出现重影或者闪烁的现象。

这个问题很容易出现,当我们的列表中有进度显示 (比如下载),这时候需要不停地更新进度,就需要使用 notifyItemChanged。使用 notifyItemChanged 可以只刷新那一个 item,这样就避免了像 ListView 那样全部刷新。但是如果使用 notifyItemChanged(position),在滑动的时候刷新就会出现重影或者闪烁的问题。

解决这个问题很简单,将 notifyItemChanged(position) 替换为 notifyItemChanged(position,0) 即可。

测试问题确实解决了,但是为啥?这个参数有啥用?这就涉及到了 RecyclerView 中的局部刷新机制,我们一起来看看源码。

recycleview 在中间新增数据 不刷新当前item位置 recyclerview刷新数据_数据_02

源码分析

我们从源码入手来看看: 

public final void notifyItemChanged(int position, @Nullable Object payload) {
    this.mObservable.notifyItemRangeChanged(position, 1, payload);
}

可以看到 payload 是一个 object,并非 int。它调用了 mObservable 的 notifyItemRangeChanged: 

public void notifyItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) {
    for(int i = this.mObservers.size() - 1; i >= 0; --i) {
        ((RecyclerView.AdapterDataObserver)this.mObservers.get(i)).onItemRangeChanged(positionStart, itemCount, payload);
    }
}

调用了 AdapterDataObserver 的 onItemRangeChanged,这是一个接口,它的实现是 RecyclerViewDataObserver,实现的函数: 

public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
    RecyclerView.this.assertNotInLayoutOrScroll((String)null);
    if(RecyclerView.this.mAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)) {
        this.triggerUpdateProcessor();
    }
}

又调用了 mAdapterHelper 的 onItemRangeChanged: 

boolean onItemRangeChanged(int positionStart, int itemCount, Object payload) {
    if(itemCount < 1) {
        return false;
    } else {
        this.mPendingUpdates.add(this.obtainUpdateOp(4, positionStart, itemCount, payload));
        this.mExistingUpdateTypes |= 4;
        return this.mPendingUpdates.size() == 1;
    }
}

调用了 obtainUpdateOp 函数: 

public AdapterHelper.UpdateOp obtainUpdateOp(int cmd, int positionStart, int itemCount, Object payload) {
    AdapterHelper.UpdateOp op = (AdapterHelper.UpdateOp)this.mUpdateOpPool.acquire();
    if(op == null) {
        op = new AdapterHelper.UpdateOp(cmd, positionStart, itemCount, payload);
    } else {
        op.cmd = cmd;
        op.positionStart = positionStart;
        op.itemCount = itemCount;
        op.payload = payload;
    }
    return op;
}

可以看到作为参数赋给一个 UpdateOp 对象,那么哪里使用了这个对象的 payload?

在 AdapterHelper 中查找发现有几处这样的代码: 

this.mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload);

这个 callback 也是一个接口,在 RecyclerView 中可以找到它的实现,其中对应的函数: 

public void markViewHoldersUpdated(int positionStart, int itemCount, Object payload) {
    RecyclerView.this.viewRangeUpdate(positionStart, itemCount, payload);
    RecyclerView.this.mItemsChanged = true;
}

调用了 viewRangeUpdate 函数: 

void viewRangeUpdate(int positionStart, int itemCount, Object payload) {
    int childCount = this.mChildHelper.getUnfilteredChildCount();
    int positionEnd = positionStart + itemCount;


    for(int i = 0; i < childCount; ++i) {
        View child = this.mChildHelper.getUnfilteredChildAt(i);
        RecyclerView.ViewHolder holder = getChildViewHolderInt(child);
        if(holder != null && !holder.shouldIgnore() && holder.mPosition >= positionStart && holder.mPosition < positionEnd) {
            holder.addFlags(2);
            holder.addChangePayload(payload);
            ((RecyclerView.LayoutParams)child.getLayoutParams()).mInsetsDirty = true;
        }
    }


    this.mRecycler.viewRangeUpdate(positionStart, itemCount);
}

可以看到调用了 holder 的 addChangePayload: 

void addChangePayload(Object payload) {
    if(payload == null) {
        this.addFlags(1024);
    } else if((this.mFlags & 1024) == 0) {
        this.createPayloadsIfNeeded();
        this.mPayloads.add(payload);
    }


}


private void createPayloadsIfNeeded() {
    if(this.mPayloads == null) {
        this.mPayloads = new ArrayList();
        this.mUnmodifiedPayloads = Collections.unmodifiableList(this.mPayloads);
    }


}


List<Object> getUnmodifiedPayloads() {
    return (this.mFlags & 1024) == 0?(this.mPayloads != null && this.mPayloads.size() != 0?this.mUnmodifiedPayloads:FULLUPDATE_PAYLOADS):FULLUPDATE_PAYLOADS;
}

这里有两个 list,mPayloads 和 mUnmodifiedPayloads,在 getUnmodifiedPayloads 中可以看到当 mPayloads 不为空才会返回 mUnmodifiedPayloads,否则返回 FULLUPDATE_PAYLOADS,即 Collections.EMPTY_LIST。

在 RecyclerView 中搜索 getUnmodifiedPayloads 函数,发现其中一处应该跟我们的问题有关: 

boolean canReuseUpdatedViewHolder(RecyclerView.ViewHolder viewHolder) {
    return this.mItemAnimator == null || this.mItemAnimator.canReuseUpdatedViewHolder(viewHolder, viewHolder.getUnmodifiedPayloads());
}

Payloads 应该对这个函数的返回值有影响,继续看 mItemAnimator 的对应函数。

这个 mItemAnimator 也是一个接口,实现类是 DefaultItemAnimator,它的对应函数: 

public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder, @NonNull List<Object> payloads) {
    return !payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads);
}

可以看到如果 payloads 不为空,即最开始的 payload 不为 null (因为是 object,所以 0 还是其它都无所谓,只要不为空就行),canReuseUpdatedViewHolder 则为 true。

那么 canReuseUpdatedViewHolder 影响什么,同样在 RecyclerView 中搜索发现: 

void scrapView(View view) {
    RecyclerView.ViewHolder holder = RecyclerView.getChildViewHolderInt(view);
    if(!holder.hasAnyOfTheFlags(12) && holder.isUpdated() && !RecyclerView.this.canReuseUpdatedViewHolder(holder)) {
        if(this.mChangedScrap == null) {
            this.mChangedScrap = new ArrayList();
        }


        holder.setScrapContainer(this, true);
        this.mChangedScrap.add(holder);
    } else {
        if(holder.isInvalid() && !holder.isRemoved() && !RecyclerView.this.mAdapter.hasStableIds()) {
            throw new IllegalArgumentException("Called scrap view with an invalid view. Invalid views cannot be reused from scrap, they should rebound from recycler pool." + RecyclerView.this.exceptionLabel());
        }


        holder.setScrapContainer(this, false);
        this.mAttachedScrap.add(holder);
    }


}

可以看到如果有 payload,holder 会被放入 mAttachedScrap,否则放入 mChangedScrap。

recycleview 在中间新增数据 不刷新当前item位置 recyclerview刷新数据_List_03

mAttachedScrap 和 mChangedScrap

这两个就涉及到 RecyclerView 的缓存机制了,整个缓存机制包含多个集合,这两个集合就是其中的重要部分,这个机制就不在这篇文章里细说了。

先看看它们两个有什么用: 

mChangedScrap

与 RecyclerView 分离的 ViewHolder 列表

mAttachedScrap

未与 RecyclerView 分离的 ViewHolder 列表

简单来说当 holder 有了变化就会放入 mChangedScrap,这样刷新的时候会移除重新 bind 一下;而 holder 没有改变则放入 mAttachedScrap,这样刷新的时候就不需要重新 bind,直接更新数据即可。

所以正是因为没有 payload 需要重新 bind,所以会出现闪烁。而在滑动中不仅位置一直变,因为进度也在变,所以不停地进行移除 bind,就会导致重影的现象。而使用了 payload 后,不会移除重新 bind,只更新进度条自己,就不会闪烁或重影了。

recycleview 在中间新增数据 不刷新当前item位置 recyclerview刷新数据_List_04

Payload 的大用处

Payload 的应用不仅仅是这么简单,在另外一个函数中也有它的身影: 

public void onBindViewHolder(@NonNull VH holder, int position, @NonNull List<Object> payloads) {
    this.onBindViewHolder(holder, position);
}

很熟悉吧,这是 RecyclerView 的 Adapter 中的一个函数,我们一般使用: 

public abstract void onBindViewHolder(@NonNull VH var1, int var2);

因为上面那个重载的函数不是 abstract 的,所以我们不容易注意到。那么这个函数有什么用?

可以看到默认处理就是调用了下面的函数,没什么特殊,但是我们可以重写它。

比如说我们刷新的时候,只想改变一个 TextView 的文案。如果是之前的处理,会重新执行一遍 onBindViewHolder(@NonNull VH var1, int var2),这样不仅那个 TextView,其它组件也会更新一遍数据 (虽然数据没变),尤其有图片的时候需要重新 load 一次。

但是重写 onBindViewHolder(@NonNull VH holder, int position, @NonNull List<Object> payloads) 我们只为 TextView 重新设置文案即可,如下: 

@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position, @NonNull List<Object> payloads) {
    if(payloads.isEmpty()){
        onBindViewHolder(holder, position);
    }
    else{
        holder.tv.setText("change text");
    }
}

而且通过对 payload 设置不同的值,我们可以通过判断 payload 分别处理不同的刷新,比如: 

@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position, @NonNull List<Object> payloads) {
    if(payloads.isEmpty()){
        onBindViewHolder(holder, position);
    }
    else{
        for(Object obj : payloads){
            if((int)obj == 1){
                holder.tv.setText("change text");
            }
            else if((int)obj == 2){
                holder.img.setImageBitmap(newBitmap);
            }
        }
    }
}

所以 payload 再配合 onBindViewHolder(@NonNull VH holder, int position, @NonNull List<Object> payloads) 使用就可以实现 RecyclerView 的 item 的局部刷新,不用再刷新整条 item 了。

recycleview 在中间新增数据 不刷新当前item位置 recyclerview刷新数据_开发者_05

总结

Payload 机制作用很大,尤其是当 RecyclerView 中的每个 Item 布局和数据比较复杂,需要单独更新的时候。使用 payload 不仅能解决闪烁和重影问题,也会使更新更高效,减少资源开销。