前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >DiffUtils让你的RecyclerView如斯顺滑

DiffUtils让你的RecyclerView如斯顺滑

原创
作者头像
用户9253515
发布2021-12-22 11:28:20
7940
发布2021-12-22 11:28:20
举报
文章被收录于专栏:Android开发技术

前言

RecyclerView的出现让我们可以实现更多更复杂的滑动布局,包括不同的布局类型,不同的数据类型。但是,越是复杂的布局,出现卡顿的现象就会越发的明显。

这其中不乏有以下几点:

  1. 无效的测量布局绘制
  2. 模版的重复初始化

通过滑动的日志分析,我们可以发现同一模版在上滑下滑的同时,会重新走onBindView方法,即使这一模版内容没有任何变化的情况下。如果在这个方法中所要执行的逻辑很多,这将会导致卡顿的出现。

原理

那么为何会重新走onBindView方法呢,你可能会说去看源码就知道了呀。没错,当你不知道它是如何实现的时候,去看源码往往是最直接有效的。但是今天这个并不是这篇文章的重点,关于RecyclerView的复用和回收网上有很多源码的解析,这里就不一一贴源码解释了,只是做一些简单的介绍。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YBFrLh9k-1640143106872)(https://github.com/lennyup/img-warehouse/raw/master/Android/recyclerview%E7%9A%84%E5%9B%9E%E6%94%B6%E5%A4%8D%E7%94%A8%E6%9C%BA%E5%88%B62.png)]

  1. RecyclerView的回收以及复用的都是ViewHolder而不是View。
  2. RecyclerView只是一个ViewGroup,其真正实现滑动的是在LayoutManager中。
  3. 回收:当一个itemView不可见时,会将其放到内存中,以便实现复用。
  4. 复用:四重奏,mChangedScrapmCacheViews、开发者自定义以及 RecycledViewPool中,都没有才会onCreatViewHolder
  5. RecyclerViewPool中的存储方式是 viewType-Array,也就是对对于每种类型最多存5个。

大部分的缓存是从recyclerViewPool中拿的,recyclerViewPool一定会走onBindViewHolder方法。这也就是回答了我们上面的提问,所以我们的思路就来了,可以通过判断数据的变化来控制onBindView中相应逻辑的执行,来提升性能。

DiffUtil主要是和RecyclerView或者ListView配合使用,由DiffUtil找出每个item的变化,由RecyclerView。Adapter更新UI。

这次优化的思路就是在onBindviewHolder中判断新旧item的变化,来做到精准更新。

实现

判断新旧数据的不同,如果数据比较复杂,那么该怎么去判断呢?我们可以用几个主要的字段来概括一下这个数据。

代码语言:javascript
复制
public interface IElement {

    /**
     * 数据内容
     * @return 返回该数据体区别于其他数据体的内容
     */
    String diffContent();
}

所有的数据bean要实现这个接口,然后在diffContent中定义自己的主要字段。不实现这个接口,用DiffUtils是没有意义的。

我们从设置数据的步骤来一步步讲解,方便理解。

当数据从网络请求回来之后,走refreshDataSource方法。

代码语言:javascript
复制
    /**
     * 刷新列表
     *
     * @param pagedList 新的列表数据
     */
    public final void refreshDataSource(List<DATA> pagedList) {
        mDiffer.submitList(pagedList);
    }

submitList中对新旧数据进行对比,并将对比结果提供给Adapter。

代码语言:javascript
复制
    /**
     *  比较数据差异,分发差异结果,调用局部刷新API,每次请求接口增加一次版本号
     * @param newList  新的数据源
     */
    public void submitList(final List<T> newList) {
        if (newList == mList) {
            // 尝试将渲染完成时机通知出去
            return;
        }

        final int runGeneration = ++mMaxScheduledGeneration;
        // 如果新集合是空 就把老集合所有都remove
        if (newList == null) {
            int countRemoved = mList.size();
            mList = null;
            mUpdateCallback.onRemoved(0, countRemoved);
            return;
        }
        // 如果老集合是空 就把新集合所有都insert
        if (mList == null) {
            mList = newList;
            updateDataSource(Collections.unmodifiableList(newList));
            mConfig.getBackgroundThreadExecutor()
                    .execute(
                            new Runnable() {
                                @SuppressLint("RestrictedApi")
                                @Override
                                public void run() {
                                    for (int i = 0; i < newList.size(); i++) {
                                        final T t = newList.get(i);
                                        if(t!=null){
                                            dataElementCache.putRecord(new ElementRecord(IDHelper.getUniqueId(t),t));
                                        }
                                    }
                                    dataElementCache.copySelf();
                                }
                            });
            mUpdateCallback.onInserted(0, newList.size());

            return;
        }

        final List<T> oldList = mList;
        mConfig.getBackgroundThreadExecutor().execute(new Runnable() {
            @SuppressLint("RestrictedApi")
            @Override
            public void run() {
                final DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() {
                    @Override
                    public int getOldListSize() {
                        return oldList.size();
                    }

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

                    @Override
                    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
                        return mConfig.getDiffCallback().areItemsTheSame(
                                oldList.get(oldItemPosition), newList.get(newItemPosition));
                    }

                    @Override
                    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
                        return mConfig.getDiffCallback().areContentsTheSame(
                                oldList.get(oldItemPosition), newList.get(newItemPosition));
                    }
                    // payload可以理解为关键的数据,就是新老item的数据中 到底哪里变化了,局部刷新某个item -- 默认返回null
                    @Override
                    public Object getChangePayload(int oldItemPosition, int newItemPosition) {
                        return mConfig.getDiffCallback().getChangePayload(
                                oldList.get(oldItemPosition), newList.get(newItemPosition));
                    }
                });
                mConfig.getMainThreadExecutor().execute(new Runnable() {
                    @Override
                    public void run() {
                        if (mMaxScheduledGeneration == runGeneration) {
                            //刷新布局
                            diffResult.dispatchUpdatesTo(mUpdateCallback);
                        }
                    }
                });
            }
        });
    }

其中updateDataSource是用来更新数据源,确保拿到的是最新的。

dataElementCache中存放的是:

代码语言:javascript
复制
private volatile ConcurrentMap<IElement,ElementRecord> elementRecords = new ConcurrentHashMap<>();

ElementRecord记录了当前的数据以及唯一标示UniqueId,将主要字段以md5方式呈现,减少耗时。

这里提供了异步进行数据比较的逻辑,mUpdateCallback实现ListUpdateCallback接口,实现adpter的刷新功能。

代码语言:javascript
复制
    @Override
    public void onChanged(int position, int count, Object payload) {
        recordChanged(position,count);
        super.onChanged(position, count, payload);
    }

    @Override
    public void onInserted(int position, int count) {
        recordChanged(position, count);
        super.onInserted(position, count);
    }

    private void recordChanged(int position, int count) {
        int  tempPosition = position;
        for (int i = 0; i <count; i++) {
            // SparseArray
            changedPositions.put(tempPosition,tempPosition);
            tempPosition++;
        }
    }

更新UI必须要在主线程中,但是DiffUtil是一个耗时的操作,所以这次用的是它的一个封装类AsyncListDifferConfig

首先,在初始化中新建Differ对象。

代码语言:javascript
复制
    /**
     * 数据项比较工具
     */
    private final IDataDiff mDataDiff;

    /** 数据比较工具 */
    private final AsyncListDifferDelegate<DATA> mDiffer;

    private final IDataCache<DATA> dataElementCache;

    public BaseSwiftAdapter(Context mContext) {
        this.mContext = mContext;
        dataElementCache = new ElementCache<>();
        final DiffCallBack diffCallBack = new DiffCallBack(dataElementCache);

        @SuppressLint("RestrictedApi") AsyncDifferConfig config =
                new AsyncDifferConfig.Builder<>(diffCallBack)
                        .setBackgroundThreadExecutor(AppExecutors.backGroudExecutors)
                        .setMainThreadExecutor(AppExecutors.mainExecutors)
                        .build();
        ChangeListCallback changedPositionCallback = new ChangeListCallback(this);
        mDataDiff = new DataDiffImpl<>(changedPositionCallback, dataElementCache);
        mDiffer =
                new AsyncListDifferDelegate(changedPositionCallback, config, dataElementCache);
    }

AsyncListDifferConfig需要三个参数:DiffUtil的内部类ItemCallback、diffUtil的item比较线程、主线程。

ItemCallback是它的抽象内部类,也就是mConfig.getDiffCallback(),看下它要实现的几个方法:

代码语言:javascript
复制
   @Override
    public boolean areItemsTheSame(IElement oldItem, IElement newItem) {
        return areContentsTheSame(oldItem, newItem);
    }

    /**
     * 总体思想是先比较对象地址,在比较内容,提高比较效率
     *
     * @param oldItem
     * @param newItem
     * @return
     */
    @Override
    public boolean areContentsTheSame(IElement oldItem, IElement newItem) {
        if (newItem == null) {
            return true;
        }
        if (oldItem == newItem) {
            return true;
        }
        recordNewElement(newItem);
        final String newContent = newItem.diffContent();
        if(newContent == null || "".equals(newContent)){
            return false;
        }

        return newContent.equals(oldItem.diffContent());
    }

areItemTheSame和areContentsTheSame,都是用来判断新旧数据是否相同,所以这里用了同一个逻辑,diffContent中存放该数据具有影响的几个字段相拼接的字符串。

dataElementCache用来存储所有数据的集合类型是IElement-ElementRecord的Array。IElement是数据本身,ElementRecord是数据的记录集,包含数据以及数据的唯一标示。

mDiffer会在后续中讲到。

我们来看下关键的onBindViewHolder中所做的事情:

代码语言:javascript
复制
    @Override
    public final void onBindViewHolder(VH holder, int position) {
        if (null != holder && holder.itemView != null) {
            tryBindData(holder, position, this.getItem(position));
        }
    }
    
    private void tryBindData(VH holder, int position, DATA newData) {
      final ElementRecord oldDataRecord = holder.content();
      boolean needBind ;
      if(needBind = (hasPositionDataRefreshChanged(oldDataRecord == null ? null : (DATA) oldDataRecord.getElement(), newData, position) || oldDataRecord == null) ){
            Log.d(getClass().getName(),"adapter onBindData 刷新或者新建"+ holder.getItemViewType());
        }else if(needBind =  hasDataContentChanged(oldDataRecord,newData)){
            Log.d(getClass().getName(),"adapter onBindData 滑动内容改变"+ holder.getItemViewType());
        }
        if(needBind){
            refreshAndBind(holder, position, newData);
        }else {
            Log.d(getClass().getName(),"adapter onBindData 复用不刷新"+ holder.getItemViewType());
        }
    }

先去判断是否是刷新变化,其次去判断是否是滑动变化,如果有变化就刷新布局,否则什么也不做。

代码语言:javascript
复制
    private boolean hasPositionDataRefreshChanged(DATA oldItem, DATA newItem, int position){
        return  mDataDiff.areItemsChanged(oldItem, newItem, position);
    }
    private boolean hasDataContentChanged(ElementRecord oldItem, DATA newItem){
        return  mDataDiff.areContentsChanged(oldItem, newItem);
    }

可以看出mDataDiff主要用来判断新旧数据是否相同。我们来实现mDataDiff中的比较:

代码语言:javascript
复制
    @Override
    public boolean areItemsChanged(T oldItem, T newItem, int position) {
        boolean changed = changedPositionCallback.hasPositionChanged(position);
        if(changed){
            changedPositionCallback.removeChangedPosition(position);
        }
        return changed;
    }

    @Override
    public boolean areContentsChanged(ElementRecord oldElementRecord, T newItem) {
        return oldElementRecord !=null  && oldElementRecord.getElement() != newItem && newItem!=null && !sameContent(oldElementRecord,newItem);
    }


    private boolean sameContent(ElementRecord oldElementRecord, T newItem){
        final ElementRecord newElementRecord = dataCache.getRecord(newItem);
        if(newElementRecord == null){
            return false;
        }
        if(IDHelper.forceRefresh(newElementRecord) || IDHelper.forceRefresh(oldElementRecord)){
            return false;
        }
        return newElementRecord.getUniqueId().equals(oldElementRecord.getUniqueId());
    }

其中比较思路为:先判断该viewHolder是否在changedPositions中,changedPositions由ChangeListCallback来提供并实现。其次判断两个对象以及唯一标示。

这里用到了两个比较类:一个是ItemCallback的比较类以及mDataDiff比较类,这里容易看混。

最关键的代码在这句:

代码语言:javascript
复制
diffResult.dispatchUpdatesTo(mUpdateCallback);

diffResult会将最小变化量提供给adpter,让其实现局部刷新。

总结

到了这里,我要讲的就差不多要结束了,希望对你们有所帮助。谢谢你们看到了这里。

相关教程

Android基础系列教程:

Android基础课程U-小结_哔哩哔哩_bilibili

Android基础课程UI-布局_哔哩哔哩_bilibili

Android基础课程UI-控件_哔哩哔哩_bilibili

Android基础课程UI-动画_哔哩哔哩_bilibili

Android基础课程-activity的使用_哔哩哔哩_bilibili

Android基础课程-Fragment使用方法_哔哩哔哩_bilibili

Android基础课程-热修复/热更新技术原理_哔哩哔哩_bilibili

本文转自 https://juejin.cn/post/6844903975796342798,如有侵权,请联系删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 原理
  • 实现
  • 总结
  • 相关教程
相关产品与服务
Elasticsearch Service
腾讯云 Elasticsearch Service(ES)是云端全托管海量数据检索分析服务,拥有高性能自研内核,集成X-Pack。ES 支持通过自治索引、存算分离、集群巡检等特性轻松管理集群,也支持免运维、自动弹性、按需使用的 Serverless 模式。使用 ES 您可以高效构建信息检索、日志分析、运维监控等服务,它独特的向量检索还可助您构建基于语义、图像的AI深度应用。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档