Android 高性能列表:RecyclerView + DiffUtil

Posted 邹奇

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 高性能列表:RecyclerView + DiffUtil相关的知识,希望对你有一定的参考价值。

文章目录

背景

  • 学习记录
  • 针对 recyclerview 实现的多数据列表展示,进一步优化数据频繁更新时的性能

介绍

  • androidSupport:v7-24.2.0 中,recyclerview 支持库开始支持了 DiffUtil 工具类的使用
  • DiffUtil 内部使用 Eugene W. Myers’s difference 算法:进行两个数据集的对比,找出新数据与旧数据之间最小的变化部分,和 RecyclerView 一起使用可以实现列表的局部更新

一般刷新 notifyDataSetChanged()

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> 
	...
	
	// 一般刷新方式
    public void notifyUpdate(List<CoderBean> mCoderList)
        this.mCoderList = mCoderList;
        if (mCoderList == null || mCoderList.size() == 0)
            this.mCoderList = new ArrayList<>();
        
        notifyDataSetChanged();
    

主要缺点:

  • 粗暴的刷新整个列表的可见区域,这时候就会触发每个 item 的视图重绘,当 onBindViewHolder(@NonNull ViewHolder holder, int position) 中的处理逻辑比较复杂,容易出现卡顿

局部刷新

为了进一步优化上面的缺点,recyclerview 提供了局部刷新的方式,如下:

# notifyItemChanged(int)
# notifyItemInserted(int)
# notifyItemRemoved(int)
# notifyItemRangeChanged(int, int)
# notifyItemRangeInserted(int, int)
# notifyItemRangeRemoved(int, int)

上面的几个 recyclerview 提供的局部刷新方法,都只会刷新指定 position 位置的 item,就不会存在一般刷新方式出现的缺点。

但是如果数据量多,且需要更新的 item 也较多,那么这将会需要我们提供较为复杂的局部刷新调用处理逻辑,这无疑是一场灾难。
所以后面 Google 也注意到了这点,后续推出了工具类: DiffUtil ,用来专门计算哪些位置的数据需要进行更新。


实现

调用代码

这里先给出调用的代码,我们来看下相关 api

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> 
	...
	
	// diff 更新方式
    public void diffUpdate(final List<CoderBean> newCoderList)
        final MyDiffUtilCallback diffUtilCallback = new MyDiffUtilCallback(this.mCoderList, newCoderList);
        // 获取差异结果(注意这里是耗时操作,如果数据量大的时候需要放到后台线程处理差异,否则会阻塞主线程)
        final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffUtilCallback);
        cloneData(newCoderList);
        // DiffResult 再把差异分发给 Adapter,adapter 最后根据接收到的差异数据做更新
        diffResult.dispatchUpdatesTo(MyAdapter.this);

    

	// 拷贝一份数据给到当前数据集 mCoderList
	private void cloneData(List<CoderBean> newCoderList) 
        this.mCoderList.clear();
        this.mCoderList.addAll(newCoderList);
    

  • 首先 MyAdapter 就是简单的展示数据逻辑:构建 itemView、获取数据,绑定数据展示
  • mCoderList 是上一次的数据集,newCoderList 是通过参数新传进来的新的数据集
  • 需要一个 DiffUtil.Callback 对象。MyDiffUtilCallback 继承了 DiffUtil.Callback 抽象类

准备工作

  • 创建实体类 CoderBean
package com.example.diffutildemo.bean;

import android.os.Parcel;
import android.os.Parcelable;

/**
 * 搬砖工 实体
 */
public class CoderBean implements Parcelable 

    private int id;
    private String name;

    public int getId() 
        return id;
    

    public void setId(int id) 
        this.id = id;
    

    public String getName() 
        return name;
    

    public void setName(String name) 
        this.name = name;
    


    @Override
    public int describeContents() 
        return 0;
    

    @Override
    public void writeToParcel(Parcel dest, int flags) 
        dest.writeInt(this.id);
        dest.writeString(this.name);
    

    public CoderBean() 
    

    protected CoderBean(Parcel in) 
        this.id = in.readInt();
        this.name = in.readString();
    

    public static final Parcelable.Creator<CoderBean> CREATOR = new Parcelable.Creator<CoderBean>() 
        @Override
        public CoderBean createFromParcel(Parcel source) 
            return new CoderBean(source);
        

        @Override
        public CoderBean[] newArray(int size) 
            return new CoderBean[size];
        
    ;



创建 MyDiffUtilCallback 类继承 DiffUtil.Callback 抽象类

代码如下:

package com.example.diffutildemo.callback;

import android.text.TextUtils;

import androidx.annotation.Nullable;
import androidx.recyclerview.widget.DiffUtil;

import com.example.diffutildemo.bean.CoderBean;

import java.util.ArrayList;
import java.util.List;

public class MyDiffUtilCallback extends DiffUtil.Callback 

    private List<CoderBean> oldCoderList = new ArrayList<>();
    private List<CoderBean> newCoderList = new ArrayList<>();

    // 通过构造传入新旧数据集
    public MyDiffUtilCallback(List<CoderBean> oldCoderList, List<CoderBean> newCoderList) 
        this.oldCoderList = oldCoderList;
        this.newCoderList = newCoderList;
    

    @Override
    public int getOldListSize() 
        return oldCoderList == null ? 0 : oldCoderList.size();
    

    @Override
    public int getNewListSize() 
        return newCoderList == null ? 0 : newCoderList.size();
    

    @Override
    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) 
        CoderBean oldCoderBean = oldCoderList.get(oldItemPosition);
        CoderBean newCoderBean = oldCoderList.get(newItemPosition);
        if (oldCoderBean != null && newCoderBean != null)
            int oldId = oldCoderList.get(oldItemPosition).getId();
            int newId = newCoderList.get(newItemPosition).getId();
            if (oldId == newId)
                return true;
            
        
        return false;
    

    @Override
    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) 
        String oldName = oldCoderList.get(oldItemPosition).getName();
        String newName = newCoderList.get(newItemPosition).getName();
        if (TextUtils.isEmpty(oldName) || TextUtils.isEmpty(newName))
            return false;
        
        if (oldName.equals(newName))
            return true;
        
        return false;
    

    @Nullable
    @Override
    public Object getChangePayload(int oldItemPosition, int newItemPosition) 
        System.out.println(":> getChangePayload +++ old: " + oldItemPosition
        + ", +++ new: " + newItemPosition);
        return super.getChangePayload(oldItemPosition, newItemPosition);
    


  • public int getOldListSize() :

返回旧列表数据集的数量。


  • public int getNewListSize():

返回新列表数据集的数量。


  • public boolean areItemsTheSame(int oldItemPosition, int newItemPosition):

两个位置的对象是否是同一个 item。一般通过实体类中定义的 id 属性值是否相同来进行判断:返回 true 表示是同一个,反之则不是。

  • public boolean areContentsTheSame(int oldItemPosition, int newItemPosition):

用来判断新旧 item 的各内容属性值是否相同(自己实现,也相对简单)。

只有当 areItemsTheSame() 返回 true 时才会触发调用:返回 true
表示是相同的各属性内容,反之则存在属性内容的变化。


  • public Object getChangePayload(int oldItemPosition, int newItemPosition):

当 areItemsTheSame() 返回 true ,并且 areContentsTheSame() 返回 false 时触发调用。

这里可以自己实现返回差异数据,会从 DiffResult 分发给 notifyItemRangeChanged(position,
count, payload) 方法,最终交给 Adapter 的 onBindViewHolder(… List< Object >
payloads) 处理。


MyAdpter 类代码实现

package com.example.diffutildemo.adatper;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;

import com.example.diffutildemo.R;
import com.example.diffutildemo.bean.CoderBean;
import com.example.diffutildemo.callback.MyDiffUtilCallback;
import com.example.diffutildemo.executor.DiffMainThreadExecutor;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> 

    private List<CoderBean> mCoderList = new ArrayList<>();
    private LayoutInflater inflater;
    private ViewHolder holder;
    private Context context;

    public MyAdapter(Context context, List<CoderBean> mCoderList) 
        this.mCoderList = mCoderList;
        this.context = context;
        this.inflater = LayoutInflater.from(context);
    

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) 
        System.out.println(":> onCreateViewHolder +++ ");
        View itemView = inflater.inflate(R.layout.recyclerview_itemview_coder, parent, false);
        holder = new ViewHolder(itemView);
        return holder;
    

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) 
        System.out.println(":> onBindViewHolder +++ " + position);
        String name = mCoderList.get(position).getName();
        holder.tv_coder.setText(name);
    

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position, @NonNull List<Object> payloads) 
//        System.out.println(":> onBindViewHolder +++ payloads");
        super.onBindViewHolder(holder, position, payloads);
    

    @Override
    public int getItemCount() 
        return (mCoderList == null) ? 0 : mCoderList.size();
    

    public class ViewHolder extends RecyclerView.ViewHolder 
        TextView tv_coder;
        public ViewHolder(@NonNull View itemView) 
            super(itemView);
            tv_coder = itemView.findViewById(R.id.tv_coder);
        
    

    @Override
    public int getItemViewType(int position) 
        return super.getItemViewType(position);
    

    // 一般刷新方式
    public void notifyUpdate(List<CoderBean> mCoderList)
        this.mCoderList = mCoderList;
        if (mCoderList == null || mCoderList.size() == 0)
            this.mCoderList = new ArrayList<>();
        
        notifyDataSetChanged();
    

    // diff 更新方式
    public void diffUpdate(final List<CoderBean> newCoderList)
        final MyDiffUtilCallback diffUtilCallback = new MyDiffUtilCallback(this.mCoderList, newCoderList);
        // 获取差异结果(注意这里是耗时操作,如果数据量大的时候需要放到后台线程处理差异,否则会阻塞主线程)
        final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffUtilCallback);
        cloneData(newCoderList);
        // DiffResult 再把差异分发给 Adapter,adapter 最后根据接收到的差异数据做更新
        diffResult.dispatchUpdatesTo(MyAdapter.this);


    

    private void cloneData(List<CoderBean> newCoderList) 
        this.mCoderList.clear();
        this.mCoderList.addAll(newCoderList);
    



  • 代码简单,不过多说明。

步骤总结

所以使用 DiffUtil 工具类进行局部刷新可以简单分为下面几步:

  • 自实现 DiffUtil.callback
  • 计算得到 DiffResult
final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffUtilCallback);
  • DiffResult 分发给 Adapter 进行局部更新
cloneData(newCoderList);
// DiffResult 再把差异分发给 Adapter,adapter 最后根据接收到的差异数据做更新
diffResult.dispatchUpdatesTo(MyAdapter.this);

计算出 DiffResult 后,咱们必须要将新数据设置给 Adapter,然后才能调用DiffResult.dispatchUpdatesTo(Adapter) 刷新ui

private void cloneData(List<CoderBean> newCoderList) 
        this.mCoderList.clear();
        this.mCoderList.addAll(newCoderList);


通过 log 证实 diffutil 的局部刷新

原始数据初始化代码:

private void initData() 
        coderList.clear();
        for (int i = 0;i < 10;i++)
            CoderBean bean = new CoderBean();
            bean.setId(i);
            bean.setName("原始数据 coder +00" + i);
            coderList.add(bean);
        
    

一般更新模拟设置数据代码:

// 一般更新数据模拟,前两个数据保持不变
    private List<CoderBean> getNewData()
        List<CoderBean> list = new ArrayList<>();
        for (int i = 0;i < 10;i++)
            CoderBean bean = new CoderBean();
            bean.setId(i);
            bean.setName("一般更新 coder +00" + i);
            if (i < 2)
                bean.setName("原始数据 coder +00" + i);
            
            list.add(bean);
        
        return list;
    

diff 更新模拟设置数据代码:

// diff 更新模拟设置数据 前两个数据保持不变
    private List<CoderBean> getNewDiffData()
        List<CoderBean> list = new ArrayList<>();
        for (int i = 0;i < 10;i++)
            CoderBean bean = new CoderBean();
            bean.setId(i);
            bean.setName("Diff更新 coder +00" + i);
            if (i < 2)
                bean.setName("原始数据 coder +00" + i);
            
            list.以上是关于Android 高性能列表:RecyclerView + DiffUtil的主要内容,如果未能解决你的问题,请参考以下文章

android联系人列表性能

Android RecyclerView嵌套RecyclerView

Android 高性能列表:RecyclerView + DiffUtil

Android 高性能列表:RecyclerView + DiffUtil

Android:扩展用户的通讯录。性能 ContentProvider vs Sqlite vs 内存中的列表

如何仅以编程方式在android中添加动态网格布局?