专栏首页逮虾户Android DiffUtil 封装|深拷贝

Android DiffUtil 封装|深拷贝

痛点以及问题

RecyclerView已经逐渐成为一个安卓开发写一个滑动布局必备的控件了,但是项目中用的大部分还是notifyDataSetChanged ,而在方法注释上其实更推荐我们直接使用增删改换这四个方法。

       /**
         * Notify any registered observers that the data set has changed.
         *
         * There are two different classes of data change events, item changes and structural
         * changes. Item changes are when a single item has its data updated but no positional
         * changes have occurred. Structural changes are when items are inserted, removed or moved
         * within the data set.
         *
         * This event does not specify what about the data set has changed, forcing
         * any observers to assume that all existing items and structure may no longer be valid.
         * LayoutManagers will be forced to fully rebind and relayout all visible views.
         *
         * RecyclerView will attempt to synthesize visible structural change events
         * for adapters that report that they have {@link #hasStableIds() stable IDs} when
         * this method is used. This can help for the purposes of animation and visual
         * object persistence but individual item views will still need to be rebound
         * and relaid out.
         *
         * If you are writing an adapter it will always be more efficient to use the more
         * specific change events if you can. Rely on notifyDataSetChanged()
         * as a last resort.
         *
         * @see #notifyItemChanged(int)
         * @see #notifyItemInserted(int)
         * @see #notifyItemRemoved(int)
         * @see #notifyItemRangeChanged(int, int)
         * @see #notifyItemRangeInserted(int, int)
         * @see #notifyItemRangeRemoved(int, int)
         */
        public final void notifyDataSetChanged() {
            mObservable.notifyChanged();
        }
复制代码

但是真实开发中,如果只是分页增加可能还简单点,我们可以用notifyItemRangeInserted去做插入操作。但是数据结构一旦发生增删替换等等,情况就会变得很复杂。谷歌也考虑到这个问题,直接让开发去做数据内容变更判断是不友善的,所以在support包中提供了DiffUtil工具给我们去做数据变更的后序开发。

Android AAC中的Paging底层也是基于DiffUtil计算的Item差异,但是我们不展开讲Paging,原因的话后面会逐步分析这个问题。

基于DiffUtil封装一下

我在文章开动之前也特地去查了一些相关文章内容,我个人看法写的都还是有点微妙的。先介绍下原理,之后我们在说一些痛点。

    public static DiffResult calculateDiff(@NonNull Callback cb, boolean detectMoves) {
     ....
    }
     /**
     * A Callback class used by DiffUtil while calculating the diff between two lists.
     */
    public abstract static class Callback {
 /**
         * Returns the size of the old list.
         *
         * @return The size of the old list.
         */
        public abstract int getOldListSize();

        /**
         * Returns the size of the new list.
         *
         * @return The size of the new list.
         */
        public abstract int getNewListSize();

        /**
         * Called by the DiffUtil to decide whether two object represent the same Item.
         * 
         * For example, if your items have unique ids, this method should check their id equality.
         *
         * @param oldItemPosition The position of the item in the old list
         * @param newItemPosition The position of the item in the new list
         * @return True if the two items represent the same object or false if they are different.
         */
        public abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition);

        /**
         * Called by the DiffUtil when it wants to check whether two items have the same data.
         * DiffUtil uses this information to detect if the contents of an item has changed.
         * 
         * DiffUtil uses this method to check equality instead of {@link Object#equals(Object)}
         * so that you can change its behavior depending on your UI.
         * For example, if you are using DiffUtil with a
         * {@link RecyclerView.Adapter RecyclerView.Adapter}, you should
         * return whether the items' visual representations are the same.
         * 
         * This method is called only if {@link #areItemsTheSame(int, int)} returns
         * {@code true} for these items.
         *
         * @param oldItemPosition The position of the item in the old list
         * @param newItemPosition The position of the item in the new list which replaces the
         *                        oldItem
         * @return True if the contents of the items are the same or false if they are different.
         */
        public abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition);
    }
复制代码

从源码分析,Callback的注释中有明确的说明,DiffUtil比较的是两个List结构。DiffUtil通过CallBack的接口(只是简单的介绍,其过程会更复杂),首先比较两个List中的size,然后逐个比较元素是不是同一个条目,也就是同一个Item,如果是同一个Item之后则比较同一个Item内的元素是不是也相同,之后生成一份DiffResult结果,开发根据这个结果进行后序的增删改等操作。

其中比较元素相同的方法是areContentsTheSame,正常情况下我们会通过模型内部定义的Id来作为模型的唯一标识符,通过这个标识符去判断这个元素是不是相同。

比较同一个元素的内容是不是相同的方法是areItemsTheSame,我直接使用的Object内的equals方法进行内容相同的判断。

痛点以及问题

首先我们需要的是两个List,其中一个代表旧数据(OldList),一个代表变更的数据(NewList)。然后两个List比较去做差异性。之后根据差异性结果来刷新adapter的内容。

我们一般在写Adapter的时候其实都是直接把List丢进来,之后就直接调用notifyDataSetChanged,这没有两个List怎么搞?很多博主的方法就是new一个新的数组之后把元数据放到这个新数组,那么我的vm或者presenter只要操作元数据就好了,这样数据变更之后我调用下刷新方法,之后让DiffUtil去做数据差异就好了。

看似我们解决了生成两个List的问题,但是因为这次拷贝只是浅拷贝,所以当元素进行areItemsTheSame不就还是自己和自己比较吗,所以只能说浅拷贝的情况下也不能完美的使用DiffUtil。

数据深拷贝

深度拷贝可以从源对象完美复制出一个相同却与源对象彼此独立的目标对象。

Paging内部的实现DiffUtil的呢,当传入List之后会生成一个快照ListSnapshotPagedList,快照就是OldList,然后对传入的List和这个快照版本进行比较,而快照的OldList就是元数据的深拷贝。其中无论是快照还是源数据,都是从DataSource获取的内容。

Pacel进行数据复制

了解过跨进程通信的老哥应该知道Parcelable的类型内容在传输中是拷贝速度是最快的,那么Parcelable是通过什么做数据拷贝的呢?

Pacel就是负责做Parcelable数据拷贝的。当不涉及到跨进程的情况下,Pacel会在内存中开辟出一个单独的区域存放Parcelable的数据内容,之后我们可以通过Parcel进行数据拷贝的操作。

            val parcelable = itemsCursor?.get(oldPosition) as Parcelable
            val parcel = Parcel.obtain()
            parcelable.writeToParcel(parcel, 0)
            parcel.setDataPosition(0)
            val constructor = parcelable.javaClass.getDeclaredConstructor(Parcel::class.java)
            constructor.isAccessible = true
            val dateEntity = constructor.newInstance(parcel) as T
            mData?.let {
                it.add(oldPosition, dateEntity)
            }
            parcel.recycle()

以上就是我拿来做数据拷贝的操作了。简单的说先构造一个Parcel.obtain()对象,然后调用源数据的writeToParcel方法,将Parcel传入到元数据内进行一次内存的粘贴操作。而每个实现了Parcelable接口的对象都有一个含有Parcel的构造函数,我们通过反射的调用这个构造函数,这样就可以生成一个新的拷贝对象。

项目分析

项目github仓库地址

先讲下为什么不用Paging,因为Paging要更换所有的Adapter,之后再传入一个Diff的Callback,这样才能用Paging的功能。但是谁家项目不封装个BaseAdapter啊,里面说不定还有header和footer之类的操作。如果要更改继承关系再传入一个参数,可能你的同事要跳起来打你膝盖了。

存粹我个人的看法哦,如果DiffUtil可以用组合的方式和当前的Adapter一起使用,这样的话是不是改造成本就是相对来说比较低的了。我们DiffUtil内部只要能完成数据拷贝,之后进行数据比较,之后通知到adapter的变更,这样我就可以根据我的需要决定那些可以先升级到Diff,哪些可以不变更。

我的封装思路是这样的,首先Diff比较的是数据模型,那么我们是不是可以对模型层进行一次增强,将其中的唯一值以及Equals方法进行抽象以及适配。

所以仓库的核心只有两个,第一个是组合,第二个是模型层的增强。

DiffHelper 组合类

class DiffHelper<T> {

    private var itemsCursor: MutableList? = null
    private var mData: CopyOnWriteArrayList? = null
    var diffDetectMoves = true
    var callBack: ListUpdateCallback? = null


    private val mMainThreadExecutor: Executor = MainThreadExecutor()

    private val mBackgroundThreadExecutor: Executor = Executors.newFixedThreadPool(2)

    private class MainThreadExecutor internal constructor() : Executor {
        val mHandler = Handler(Looper.getMainLooper())
        override fun execute(command: Runnable) {
            mHandler.post(command)
        }
    }

    fun setData(itemsCursor: MutableList<T>?, ignore: Boolean = false) {
        this.itemsCursor = itemsCursor
        itemsCursor?.apply {
            mBackgroundThreadExecutor.execute {
                if (mData == null) {
                    copyData()
                }
                if (!ignore) {
                    mMainThreadExecutor.execute {
                        callBack?.onInserted(0, itemsCursor.size)
                    }
                }
            }
        }
    }

    private fun copyData() {
        try {
            itemsCursor?.apply {
                if (isNotEmpty()) {
                    if (this[0] is Parcelable) {
                        mData = CopyOnWriteArrayList()
                    } else {
                        mData = CopyOnWriteArrayList()
                        for (entity in this) {
                            mData?.add(entity)
                        }
                        return
                    }
                } else {
                    mData = CopyOnWriteArrayList()
                    return

                }
                for (entity in this) {
                    val parcel = Parcel.obtain()
                    (entity as Parcelable).writeToParcel(parcel, 0)
                    parcel.setDataPosition(0)
                    val constructor = entity.javaClass.getDeclaredConstructor(Parcel::class.java)
                    constructor.isAccessible = true
                    val dateEntity = constructor.newInstance(parcel) as T
                    mData?.add(dateEntity)
                    parcel.recycle()
                }
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    fun notifyItemChanged() {
        mBackgroundThreadExecutor.execute {
            val diffResult = diffUtils()
            mMainThreadExecutor.execute {
                diffResult.dispatchUpdatesTo(object : ListUpdateCallback {
                    override fun onInserted(position: Int, count: Int) {
                        callBack?.onInserted(position, count)
                    }

                    override fun onRemoved(position: Int, count: Int) {
                        callBack?.onRemoved(position, count)
                    }

                    override fun onMoved(fromPosition: Int, toPosition: Int) {
                        callBack?.onMoved(fromPosition, toPosition)
                    }

                    override fun onChanged(position: Int, count: Int, payload: Any?) {
                        callBack?.onChanged(position, count, payload)
                    }
                })
            }
        }
    }

    @Synchronized
    private fun diffUtils(): DiffUtil.DiffResult {
        val diffResult =
                DiffUtil.calculateDiff(BaseDiffCallBack(mData, itemsCursor), diffDetectMoves)
        copyData()
        return diffResult

    }

    fun getItemSize(): Int {
        return itemsCursor?.size ?: 0
    }

    fun  getEntity(pos: Int): T? {
        return if (itemsCursor?.size ?: 0 <= pos || pos < 0) null else itemsCursor?.get(pos) as T
    }

}
复制代码

我先抽象出一个代理方法,将数据源内容包裹起来,对外提供四个方法,setData, notifyItemChanged,getItemSize,getEntity这四个方法。

  1. setData 设置数据源方法,设置数据源的同时进行数据第一次拷贝操作。
  2. notifyItemChanged 该方法直接调用DiffUtil,当数据源内容发生变更时,调用该方法,会通过接口回掉的方式通知Adapter的变更。
  3. getItemSize adapter获取当前数据源的长度,替换掉adapter内部的size方法。
  4. getEntity 获取数据实体类型。

抽象统一的Model

class BaseDiffCallBack(private val oldData: List<*>?, private val newData: List<*>?) : DiffUtil.Callback() {

    override fun getOldListSize(): Int {
        return oldData?.size ?: 0
    }

    override fun getNewListSize(): Int {
        return newData?.size ?: 0
    }

    override fun areItemsTheSame(p0: Int, p1: Int): Boolean {
        val object1 = oldData?.get(p0)
        val object2 = newData?.get(p1)
        if (object1 == null || object2 == null)
            return false
        return if (object1 is IDifference && object2 is IDifference) {
            TextUtils.equals(object1.uniqueId, object2.uniqueId)
        } else {
            object1 == object2
        }
    }


    override fun areContentsTheSame(p0: Int, p1: Int): Boolean {
        val object1 = oldData?.get(p0)
        val object2 = newData?.get(p1)
        return if (object1 is IEqualsAdapter && object2 is IEqualsAdapter) {
            object1 == object2
        } else {
            true
        }
    }

}
复制代码

这个类就是抽象出来的模型层比较的。

  1. areItemsTheSame方法比较的是模型层是不是实现了IDifference,通过比较这个接口来进行唯一值比较。
  2. areContentsTheSame 则是根据当前模型层是不是实现了IEqualsAdapter,如果没有实现则标示不需要比较值内容,如果有则直接比较值内容。

TODO

其实一个Diff调用的时间相对来说是比较耗时的,这一块我没咋搞,讲道理是可以用async的方式去实现的。可以去优化一下这方面。

刚刚参考了下AsyncListDiffer写完了,有的抄的情况下,还是很容易的。

如何使用

数据模型的定义,首先必须实现Parcelable(深拷贝的逻辑),然后必须实现IDifference接口,主要来辨别数据主体是否发生变更。

(可选) IEqualsAdapter实现了该接口之后当数据内容发生变更,也会通知Adapter刷新。我们可以通过IDEA插件或者kt的data去实现模型的equals方法,去做元素内容相同的比较,毕竟equals方法写起来还是很恶心的。

data class TestEntity(var id: Int = 0,
                      var displayTime: Long = 0,
                      var text: String? = Random().nextInt(10000).toString()) : Parcelable, IDifference, IEqualsAdapter {

    override val uniqueId: String
        get() = id.toString()

    fun update() {
        displayTime = System.currentTimeMillis()
        text = "更新数据"
    }


    constructor(source: Parcel) : this(
            source.readInt(),
            source.readLong(),
            source.readString()
    )

    override fun describeContents() = 0

    override fun writeToParcel(dest: Parcel, flags: Int) = with(dest) {
        writeInt(id)
        writeLong(displayTime)
        writeString(text)
    }

    companion object {
        @JvmField
        val CREATOR: Parcelable.Creator = object : Parcelable.Creator {
            override fun createFromParcel(source: Parcel): TestEntity = TestEntity(source)
            override fun newArray(size: Int): Array = arrayOfNulls(size)
        }
    }
}
复制代码

初始化并传入数据,并设置数据刷新回掉,如果你有header或者别的话自己定义一个。

     val diffHelper: DiffHelper = DiffHelper()
     diffHelper.callBack = SimpleAdapterCallBack(this)
     diffHelper.setData(items)

当list发生变化(任意变化增删改都行),调用数据刷新。

   diffHelper.notifyItemChanged()

完了

文章的结尾我打算给大家讲个故事,从前有个太监

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 这个列表实现很复杂?不存在

    https://github.com/crazysunj/MultiTypeRecyclerViewAdapter

    陈宇明
  • 架构设计基础知识整理

    From http://msdn.microsoft.com/en-us/library/ff647859.aspx

    哲洛不闹
  • AsyncListDiffer-RecyclerView最好的伙伴

    导读,近些年来 Android 一直在优化 RecyclerView 刷新效率,相继出了 DiffUtil,AsyncListDiffer ,我在我的开源库 F...

    程序亦非猿
  • Unity编译Android的原理解析和apk打包分析

    最近由于想在Scene的脚本组件中,调用Android的Activity的相关接口,就需要弄明白Scene和Activity的实际对应关系,并对Unity调用A...

    张坤
  • 这些JS工具函数够你用到2020年底了

    Vam的金豆之路
  • Java 字节的常用封装一. Java 的字节二. 常用封装三. mmap 的操作四. 总结

    byte (字节) 是 Java 中的基本数据类型,一个 byte 包含8个 bit(位),byte 的取值范围是-128到+127。

    fengzhizi715
  • Binder概述,快速了解Binder体系

    众所周知,Binder是Android系统中最主要的进程间通信套件,更具体一点,很多文章称之为Binder驱动,那为什么说它是一个驱动呢,驱动又是何物,让我们自...

    做个快乐的码农
  • Android中利用zxing实现自己的二维码扫描识别详解

    在上一篇文章中已经简单介绍了zxing的使用,快速集成到自己的项目中,但是使用的扫描我们没办法根据自己的需求来做自己的扫描界面,所以这篇我们来学习一下如何根据自...

    砸漏
  • Android Architecture Paging Library详解 | Google I/O大会上的最新发布

    Android高级工程师,6年以上开发经验,有丰富的代码重构和架构设计经验,负责京东商城我的京东的开发工作,热衷于学习和研究新技术。

    京东技术
  • JavaSE面试深度剖析 第一讲

    注意:默认情况下面向对象有 3 大特性,封装、继承、多态,如果面试官问让说出 4 大特性,那么我们就把抽象加上去。

    易兮科技
  • 开源计划之--Android绘图库--LogicCanvas

    Painter采用单例模式 优化原型模式,各Shape采用深拷贝来解决构造较长、繁琐的情况 比较new 对象和拷贝的效率问题,拷贝一点。具体见文:来谈谈Ja...

    张风捷特烈
  • 我也想聊聊Binder机制

    想写篇关于Binder的文章,可对其一无所知,无从下手。在阅读了大量的优秀文章后,心惊胆战的提笔,不怕文章被贻笑大方,怕的是误人子弟!望各位大佬抽空阅读本文的同...

    Rouse
  • js对象的直接赋值、浅拷贝与深拷贝

    最近Vue项目中写到一个业务,就是需要把对话框的表单中的数据,每次点击提交之后,就存进一个el-table表格中,待多次需要的表单数据都提交进表格之后,再将这个...

    OwenZhang
  • android binder机制详解

    摘要 Binder是android中一个很重要且很复杂的概念,它在系统的整体运作中发挥着极其重要的作用,不过本文并不打算从深层次分析Binder机制,有两点原...

    xiangzhihong
  • Android-Binder机制

    Binder机制是​ Android系统中进程间通讯(IPC)的一种方式,Android中ContentProvider、Intent、aidl都是基于Bind...

    用户7557625
  • 飞桨PaddleOCR C++预测库布署

    关于OCR这块以前《Android通过OpenCV和TesserartOCR实时进行识别》中用过TesserartOCR,原来用的模型库也挺大,最近也研究了下别...

    Vaccae
  • 深入理解Binder

    之前一直对 Binder 理解不够透彻,仅仅知道一些皮毛,所以最近抽空深入理解一下,并在这里做个小结。

    俞其荣
  • 深入理解Binder

    之前一直对 Binder 理解不够透彻,仅仅知道一些皮毛,所以最近抽空深入理解一下,并在这里做个小结。

    俞其荣
  • MMKV为什么可以替换SharedPreferences

    MMKV——基于 mmap 的高性能通用 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。 https://git...

    马上就说

扫码关注云+社区

领取腾讯云代金券