美文网首页
RecyclerView中 各种节能刷新 和重点DiffUtil

RecyclerView中 各种节能刷新 和重点DiffUtil

作者: 大川的川 | 来源:发表于2020-04-10 13:56 被阅读0次
你瞅啥?
RecyclerView的刷新基本分为以下两种情况:
  • 1. 如果大量的数据被修改或者被修改数据的位置不确定,这个方法很消耗性能,不到万不得已不要使用,请尽量使用下面的刷新方法。实现如下:
adapter.notifyDataSetChanged();
  • 2. 刷新某一项,定点刷新(常用),消耗性能很少,但是会有定位的问题,可能在过程中需要遍历集合获取操作下标
//刷新某Item中的所有组件
adapter.notifyItemChanged(position);
//刷新某Item中的部分组件
adapter.notifyItemChanged(position, payloads);
//插入Item
adapter.notifyItemInserted(position);       
//删除Item
adapter.notifyItemRemoved(position);  
//移动Item
adapter.notifyItemMoved(position, position + 1);

RecyclerView的刷新问题Google推出了DiffUtil这个解决方案:

  • DiffUtil的运用逻辑非常简单,大致如下:
    实现对比新旧数据的方法(类似比较器),这样DiffUtil便知道当新数据来临时,该不该更新某个item。
    更新数据时,把新旧数据丢给DiffUtil,底层会根据你实现的对比方法,利用一种差分算法自动计算出差异,最后局部更新到UI。
  • DiffUtil的使用也很简单:
    1、先实现比较新旧数据的回调,可以是一个独立的类,也可以写成Adapter的内部类:
public class BaseXXXAdapter<T> extends RecyclerView.Adapter {
    // ...

    private class DiffCallback extends DiffUtil.Callback {
        private List<T> oldData, newData;

        DiffCallback(List<T> oldData, List<T> newData) {
            this.oldData = oldData;
            this.newData = newData;
        }

        @Override
        public int getOldListSize() {
            return oldData.size();
        }

        @Override
        public int getNewListSize() {
            return newData.size();
        }

        @Override
        public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
            T oldT = oldData.get(oldItemPosition);
            T newT = newData.get(newItemPosition);
            // 实际情况最好是在此处对比新旧数据的id(比如用户uid),这里为了方便示例直接equals对象了
            // 若此处返回true,则DiffUtil会再调用下面的areContentsTheSame方法,进一步对比UI是否有变化
            // 若此处返回false,则说明id都不同,肯定不是一个item
            return Objects.equals(oldT, newT);
        }

        @Override
        public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
            // TODO 比较新旧数据(主要是UI展示内容)是否相同,这里为了方便示例直接返回true
            return true;
        }
    }
}

2、然后在Adapter内部实现一个update数据的方法:

    @Override
    public void updateData(List<T> newData) {
        DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffCallback(getData(), newData));
        // 这里的getData即表示获取整个列表的数据,自行实现即可
        getData().clear();
        getData().addAll(newData);
        result.dispatchUpdatesTo(this);
    }

3、重点还是 areItemsTheSame 和 areContentsTheSame 方法,后者大部分时候只需要对比每个item上UI展示出来的数据即可,因为用户只关心眼见的内容。

解决使用后产生的问题:
我们会发现在上面的使用示例中,updateData 方法内部对原数据进行了清除和添加的操作,这会导致一个问题便是:列表数据集合中的对象已经变了,即使其某项对应的UI内容没有发生变化。
举个例子,一个通讯录列表里面有 [小明, 小红] 两个人,对应内存地址为 [a1, a2],现在通过上述 updateData 方法更新了通讯录列表,UI内容变成了 [小王, 小红],对应内存地址为 [b1, b2]。对用户来说小红这个item看上去没有发生变化,但其实对应的数据类对象已经不同。而且此时 onBindViewHolder 方法只会触发一次,将小明更新成小王,而不会触发小红那个position对应的 onBindViewHolder 。
上述细节很关键,如果开发过程中绑定(bind)数据不恰当的话,就容易造成各种奇异问题,比如网上资料最多的DiffUtil导致item点击事件数据错位问题、数组越界崩溃问题等等。
这里的“不恰当”,绝大部分情况下,总结出来:其实指的就是在 onBindViewHolder 方法中持有了某个位置(position)对应数据的不可变对象。最常见的误用示例就是在 onBindViewHolder 中设置某些控件的点击事件并引用数据对象:

    // 此处假设item的数据类为User
    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        MyItemViewHolder h = (MyItemViewHolder) holder;
        User user = getData().get(position);
        h.mNameTextView.setOnClickListener(v -> {
            // 第2种写法:User user = getData().get(position);
            // 假设这里是点击item跳转到该User对应的个人主页界面
            startWebView(user.getHomePageUrl());
        });
    }

在不接入DiffUtil之前,上面这段代码没有任何问题,因为我们都是使用 notifyDataSetChanged 方法来更新UI,每次更新调用到 onBindViewHolder 时,点击事件都会重新设置,get出来的user对象自然也是最新的。一旦我们使用了DiffUtil,就会出问题了。
回到上面小王绿了小明的例子,在我们的 updateData 方法执行后,如果我们只对比了user的名字这个属性(其实也只需要对比这个属性),那么小红那一个item就不会触发对应的 onBindViewHolder ,即小红的点击事件回调里,仍然持有着旧数据集的user对象(对应那个内存地址a2)。但实际上小红应该对应 b2 那个内存了,这就造成 a2 内存无法释放,问题是不是显得有点严重了。
有同学说无所谓呀,反正点击事件依然有效。那如果我说网络数据刷新下来小红的 homePageUrl 变了呢?是不是还得把这个属性加入DiffUtil的对比方法中?这样最终会导致小红的 onBindViewHolder 方法也执行,跟 notifyDataSetChanged 岂不是没什么两样了?
此外,若get对象写成注释中的第2种写法,且列表第0个位置的item被删了呢?小红顶上去变成了第0个,此时由于小红的UI内容没变,只是位置变了,所以 onBindViewHolder 依然不会执行。以上面的示例代码来看,当再次点击小红时,就会直接出现数组越界的异常。因为position还是之前的1,而此时小红的position已经为0。

显然上述出现的这些问题不符合谷歌的设计初衷,也不符合我们使用DiffUtil的初衷。其实解决办法很简单,就是要对 onBindViewHolder 方法有一个正确的认知,其原则就是:

  • onBindViewHolder 只做UI内容的更新,如 setText,setImageXXX 等方法。做到数据对象一次性使用。
  • 不要跨作用域持有与位置(position)相关的数据,比如每个item的数据对象。尤其就是避免在 onBindViewHolder 中设置点击事件监听。

正确的点击事件监听还是参照如下形式比较好:

// 比如这是某个Base适配器类
public class BaseXXXAdapter<T> extends RecyclerView.Adapter {
    // ...
    private View.OnClickListener mOnClickListener;
    private View.OnLongClickListener mOnLongClickListener;
    private OnItemClickListener mOnItemClickListener;

    public interface OnItemClickListener {
        void onItemClick(View view, RecyclerView.ViewHolder holder, int position);

        void onItemLongClick(View view, RecyclerView.ViewHolder holder, int position);
    }

    public BaseXXXAdapter(Context context) {
        // ...
        mOnClickListener = v -> {
            RecyclerView.ViewHolder h = (RecyclerView.ViewHolder) v.getTag();
            int pos = h.getAdapterPosition();
            if (mOnItemClickListener != null) {
                mOnItemClickListener.onItemClick(v, h, pos);
            }
        };
        mOnLongClickListener = v -> {
            RecyclerView.ViewHolder h = (RecyclerView.ViewHolder) v.getTag();
            int pos = h.getAdapterPosition();
            if (mOnItemClickListener != null) {
                mOnItemClickListener.onItemLongClick(v, h, pos);
            }
            return true;
        };
    }

    public void setOnItemClickListener(OnItemClickListener clickListener) {
        this.mOnItemClickListener = clickListener;
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        // ...省略holder实例化
        holder.itemView.setTag(holder); // 把holder当tag存
        holder.itemView.setOnClickListener(mOnClickListener);
        holder.itemView.setOnLongClickListener(mOnLongClickListener);
        return holder;
    }
}

// 继承实现的实际业务Adapter
public class XXXAdapter extends BaseXXXAdapter<User> {
    public XXXAdapter(Context context) {
        setOnItemClickListener(new OnItemClickListener() {
            @Override
            public void onItemClick(View view, RecyclerView.ViewHolder holder, int position) {
                MyItemViewHolder h = (MyItemViewHolder) holder;
                // 每次点击都保证了为对应位置的数据,再也不用担心数据错位问题了
                User user = getData().get(position);
            }

            @Override
            public void onItemLongClick(View view, RecyclerView.ViewHolder holder, int position) {
                // ...
            }
        });
    }
}

参考来源:掘金 作者:针叶
参考来源:简书 作者:BruceBug

注:参考记录目的是为了加深印象和自己以后方便查阅

相关文章

网友评论

      本文标题:RecyclerView中 各种节能刷新 和重点DiffUtil

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