• 记录一次项目中的《Recyclerview的优化》


    前言

    看这篇文章可以让你了解到:在一个复杂的RecyclerView中,有数百个Item,每个Item都包含大量的数据和图像。如何有效地加载和显示这些数据,同时保持列表的平滑滚动?

    整体结构

    问题的开端:

    在一个在线购物APP上遇到的问题(不是多点APP),有数百个商品的展示的Item,每个Item都有大量的数据和图片展示。也就是数据量很大,导致商品列表加载和显示过程很慢。

    定位问题:

    • Window.addOnFrameMetricsAvailableListener() 相当于adb shell dumpsys gfxinfo 精细的获取渲染时间,精确到秒
    • Android 的 Profiler

    问题的解决:

    • 高效的更新:DiffUtil
    • 分页的预加载(通过滑动的监听)
    • 设置公用的addXxListener监听(点击、长按等等)
    • 其他优化:在开发中就要注意到的
      • 增加缓存,空间换时间:RecycleView.setItemViewCacheSize(size);
      • 滑动过程中的加载图片策略:不要简单根据滑动状态判断,建议通过滑动速度、惯性滑动来判断。
      • 减少XML的解析时间,能通过new view创建再添加的视图,就可以替换掉XML中的布局View
      • 减少渲染层级:利用自定义View(特别不推荐ConstraintLayout,他在初次渲染渲染很慢)
      • 能固定高度的Item就固定高度,减少测量时间
        • RecyclerView.setHasFixedSize(true) 的作用是告诉 RecyclerView,其中的项(Item)的大小在途中不会改变。
      • 没有动画要求,就把默认动画关掉
        • ((SimpleItemAnimator) rv.getItemAnimator()).setSupportsChangeAnimations(false); 把默认动画关闭

    其他解决方案:

    • 用动态构建Kotlin 的 DSL 布局取代 xml
      蒸发 IO 和 反射的性能损耗,缩短构建表项布局耗时。有点过了,减少了代码可读性,毕竟不能预览界面。还有其他人员后续维护。 时间减少几十ms 得不偿失。
    • Paging 3
      这两种解决方案都是同一个问题:有学习、维护的成本。特别对于现有项目来说,改动过大,牵扯业务过多,出现问题难定位。

    高效的更新DiffUtil

    什么是DiffUtil

    DiffUtil 是一个用于计算两个列表之间差异的实用工具类。它通过比较两个列表的元素,找出它们之间的差异,并生成更新操作的列表,以便进行最小化的更新操作。
    当然这种最小化的更新操作完全可以通过严格的去使用notify相关的API去控制,所以我认为DiffUtil是一种最小化更新操作的规范形式。(ps:毕竟难免的会错误的触发notify导致资源的浪费)

    注意:强调的是DiffUtil的更新,如果只是单独的添加还是希望去用notifyItemInserted(),单独的添加的操作在业务中你肯定是知道的。
    原理:了解一下就可以了:DiffUtil 使用最长公共子序列(Longest Common Subsequence,LCS)算法来比较两个数据集之间的差异。算法首先创建会一个矩阵,矩阵的行表示旧数据集的元素,列表示新数据集的元素。之后通过回溯构建最长公共子序列,通过比较不属于最长公共子序列的元素,来确定两个数据集之间的差异。

    核心类DiffUtil.Callback

    我们在使用DiffUtil也是主要去使用DiffUtil.Callback,他掌握着重要的监控差异性的几个抽象方法。

    • getOldListSize():获取旧数据集的大小
    • getNewListSize():// 获取新数据集的大小
    • areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean
      • 分别获取新老列表中对应位置的元素,并定义什么情况下新老元素是同一个对象
    • areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean
      • 分别获取新老列表中对应位置的元素,并定义什么情况下同一对象内容是否相同
    • getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any?
      • 分别获取新老列表中对应位置的元素,如果这两个元素相同,但是内容发生改变,可以通过这个方法获取它们之间的差异信息,从而只更新需要改变的部分,减少不必要的更新操作。

    具体的代码和解释如下

    class RvAdapter : RecyclerView.Adapter<RvAdapter.ViewHolder>() {
        class ViewHolder(itemView : View) : RecyclerView.ViewHolder(itemView)
    
        var oldList: MutableList<MessageData> = mutableListOf() // 老列表
        var newList: MutableList<MessageData> = mutableListOf() // 新列表
    
        val diffUtilCallBack = object : DiffUtil.Callback(){
            override fun getOldListSize(): Int {
                // 获取旧数据集的大小
                return oldList.size
            }
    
            override fun getNewListSize(): Int {
                // 获取新数据集的大小
                return newList.size
            }
    
            override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
                // 分别获取新老列表中对应位置的元素
                // 定义什么情况下新老元素是同一个对象(通常是业务id)
                val oldItem = oldList[oldItemPosition]
                val newItem = newList[newItemPosition]
                return oldItem.id == newItem.id
            }
    
            override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
                // 定义什么情况下同一对象内容是否相同 (由业务逻辑决定)
                // areItemsTheSame() 返回true时才会被调用
                val oldItem = oldList[oldItemPosition]
                val newItem = newList[newItemPosition]
                return oldItem.content == newItem.content
            }
    
            override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
                // 可以通过这个方法获取它们之间的差异信息
                // 具体定义同一对象内容是如何地不同 (返回值会作为payloads传入onBindViewHoder())
                // 当areContentsTheSame()返回false时才会被调用
                val oldItem = oldList[oldItemPosition]
                val newItem = newList[newItemPosition]
                return if (oldItem.content === newItem.content) null else newItem.content
            }
        }
    
        fun upDataList(oldList: MutableList<MessageData>, newList: MutableList<MessageData>){
            this.oldList = oldList
            this.newList = newList
            // 利用DiffUtil比对结果
            val diffResult = DiffUtil.calculateDiff(diffUtilCallBack)
            // 将比对结果应用到 adapter
            diffResult.dispatchUpdatesTo(this)
        }
        
        // 其他常规的函数
        .....
        .....
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    简单看一下源码

    就看一下diffResult.dispatchUpdatesTo(this)做了什么吧。差异性的算法刚才上面说了一嘴

    // 将比对结果应用到Adapter
    public void dispatchUpdatesTo(final RecyclerView.Adapter adapter) {
        dispatchUpdatesTo(new AdapterListUpdateCallback(adapter));
    }
    
    // 将比对结果应用到ListUpdateCallback
    public void dispatchUpdatesTo(@NonNull ListUpdateCallback updateCallback) {...}
    
    // 基于 RecyclerView.Adapter 实现的列表更新回调
    public final class AdapterListUpdateCallback implements ListUpdateCallback {
        private final RecyclerView.Adapter mAdapter;
        public AdapterListUpdateCallback(@NonNull RecyclerView.Adapter adapter) {
            mAdapter = adapter;
        }
        @Override
        public void onInserted(int position, int count) {
                // 区间插入
            mAdapter.notifyItemRangeInserted(position, count);
        }
        @Override
        public void onRemoved(int position, int count) {
                // 区间移除
            mAdapter.notifyItemRangeRemoved(position, count);
        }
        @Override
        public void onMoved(int fromPosition, int toPosition) {
                // 移动
            mAdapter.notifyItemMoved(fromPosition, toPosition);
        }
        @Override
        public void onChanged(int position, int count, Object payload) {
                // 区间更新
            mAdapter.notifyItemRangeChanged(position, count, payload);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    所以我上面说了:我认为DiffUtil是一种最小化更新操作的规范形式。

    异步化Diff计算过程

    上面说了,是通过算法进行计算,来统计我们的差异性。那当遇到大数据,难免的会遇到计算带来的耗时问题。
    所以将这个Diff过程进行异步处理,是有必要做的。(ps:直接用协程得了)

    suspend fun upDataList(oldList: MutableList<MessageData>, newList: MutableList<MessageData>) =
        withContext(Dispatchers.Default) {
            this@RvAdapter.oldList = oldList
            this@RvAdapter.newList = newList
    
            // 利用DiffUtil比对结果
            val diffResult = DiffUtil.calculateDiff(diffUtilCallBack)
            withContext(Dispatchers.Main) {
                // 将比对结果应用到 adapter
                diffResult.dispatchUpdatesTo(this@RvAdapter)
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    • 异步操作下要注意线程安全问题:可以使用Mutex来保护oldList和newList的访问和修改

    修改后的代码

    private val updateListMutex = Mutex()
    
    suspend fun upDataList(oldList: MutableList<MessageData>, newList: MutableList<MessageData>) = withContext(Dispatchers.Default) {
        // 加锁,保护数据的访问和修改
        updateListMutex.withLock {
            this@RvAdapter.oldList = oldList
            this@RvAdapter.newList = newList
        }
    
        // 利用DiffUtil比对结果
        val diffResult = DiffUtil.calculateDiff(diffUtilCallBack)
    
        withContext(Dispatchers.Main) {
            // 加锁,保护数据的访问和修改
            updateListMutex.withLock {
                // 将比对结果应用到 adapter
                diffResult.dispatchUpdatesTo(this@RvAdapter)
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • DiffUtil是通过比较两个数据对象的引用来判断它们是否相同的

    如果你用的都是不可变的对象,也就是Final修饰的那就没问题。
    如果是可变对象,那么你要重写equals和hashCode方法以便DiffUtil正确比较数据项,具体代码按实际业务来。

    分页的预加载

    优化的思路

    当然分页加载数据是必须项:关于列表的内容,都需要由服务器返回的分页数据。这样避免了一次性加载过度数据带来的请求延迟。也减轻了服务器的压力。
    那我们要怎么优化这个分页呢?
    既然预加载作为我们优化加载速度重要的一个思想。那么在分页中是不是也可以加入这个思想呢?
    也就是说:在一页数据还未看完时就请求下一页数据。那么我们可以通过两种思想去做:

    • 在一页数据还未看完时就请求下一页数据
    • 第一次请求2页内容,当滑动过当前页所有Item时,就请求后续页的内容(当然这个预加载的页数也可以是3或者更多)

    实现(第一种)

    两种方法都是提前加载下一页的数据,来进行优化用户的感知。
    我们这里只说一下第一种方式,第二种方式是类似的。

    • 第一步:重写RecyclerView的Adapter监听列表的绑定Item的position,当达到阈值时去请求数据
    class PreloadAdapter : RecyclerView.Adapter<ViewHolder>() {
    
        override fun onBindViewHolder(holder: ViewHolder, position: Int) {
            checkPreload(position)    // 判断是否达到阈值
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    注意:这里对RecyclerView了解的可能会问,那onBindViewHolder会在RecyclerView预加载的时候就会被回调。并不是当前Item显示在页面的时候。
    答:当然,但是第一点你可以去设置RecyclerView预加载的个数,第二点如果预加载的时候就会被回调那么请求被提前了,有什么不好呢?

    • 第二步:监听滑动状态,当确定是滑动触发时再加载
    override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
        recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
                // 更新滚动状态
                scrollState = newState
                super.onScrollStateChanged(recyclerView, newState)
            }
        })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 第三步:防止列表滚动到底部触发了一次预加载后,又往回滚动。 再次滚下来,当预加载未完成,会再次触发的风险。
    // 增加预加载状态标记位
    var isPreloading = false
    
    • 1
    • 2
    • 完整代码
    class PreloadAdapter : RecyclerView.Adapter<ViewHolder>() {
        // 增加预加载状态标记位
        var isPreloading = false
        // 预加载回调
        var onPreload: (() -> Unit)? = null
        // 预加载偏移量
        var preloadItemCount = 0
        // 列表滚动状态
        private var scrollState = SCROLL_STATE_IDLE
    
        override fun onBindViewHolder(holder: ViewHolder, position: Int) {
            checkPreload(position)
        }
    
        override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
            recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
                override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
                    // 更新滚动状态
                    scrollState = newState
                    super.onScrollStateChanged(recyclerView, newState)
                }
            })
        }
    
        // 判断是否进行预加载
        private fun checkPreload(position: Int) {
            if (onPreload != null
                && position == max(itemCount - 1 - preloadItemCount, 0)// 索引值等于阈值
                && scrollState != SCROLL_STATE_IDLE // 列表正在滚动
                && !isPreloading // 预加载不在进行中
            ) {
                isPreloading = true // 表示正在执行预加载
                onPreload?.invoke()
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 第四步:调用
    val preloadAdapter = PreloadAdapter().apply {
        // 在距离列表尾部还有2个表项的时候预加载
        preloadItemCount = 2
        onPreload = {
            // 预加载业务逻辑
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    使用公共监听

    我们可以利用自定义公共的监听来减少监听对象的创建时间,提高性能,并且使用 holder.getAdapterPosition() 方法获取准确的 ID 或 Tag 进行判断。

    错误的做法

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.itemText.text = mItemList[position]
        holder.itemText.setOnClickListener({ 
            // 具体点击业务
        })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这样会在每次绑定View,也就是执行onBindViewHolder都去对itemText设置监听对象,这样大量的频繁的创建对象,你这是要干嘛!!!!

    建议的做法

    • 第一步:首先,在 RecyclerView 的适配器中定义一个接口,作为公用的监听器:
    interface RecyclerViewListener {
        fun onItemClick(position: Int)
        fun onItemLongClick(position: Int)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 第二步:然后,在 RecyclerView 的 ViewHolder 中设置监听器:
    class RecyclerViewHolder(itemView: View, private val listener: RecyclerViewListener) : RecyclerView.ViewHolder(itemView),
        View.OnClickListener, View.OnLongClickListener {
        private val textView: TextView = itemView.findViewById(R.id.textView)
    
        init {
            itemView.setOnClickListener(this)
            itemView.setOnLongClickListener(this)
        }
    
        override fun onClick(v: View) {
            val position = adapterPosition
            listener.onItemClick(position)
        }
    
        override fun onLongClick(v: View): Boolean {
            val position = adapterPosition
            listener.onItemLongClick(position)
            return true
        }
    
        fun bindData(data: String) {
            textView.text = data
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 第三步:接下来,在适配器中设置监听器:
    class RecyclerViewAdapter(private val dataList: List<String>, private val listener: RecyclerViewListener) :
        RecyclerView.Adapter<RecyclerViewHolder>() {
    
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerViewHolder {
            val itemView = LayoutInflater.from(parent.context).inflate(R.layout.item_layout, parent, false)
            return RecyclerViewHolder(itemView, listener)
        }
    
        override fun onBindViewHolder(holder: RecyclerViewHolder, position: Int) {
            val data = dataList[position]
            holder.bindData(data)
        }
    
        override fun getItemCount(): Int {
            return dataList.size
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 第四步:最后,在使用 RecyclerView 的地方,设置公用的监听器并创建适配器:
    val listener = object : RecyclerViewListener {
        override fun onItemClick(position: Int) {
            // 处理点击事件
        }
    
        override fun onItemLongClick(position: Int) {
            // 处理长按事件
        }
    }
    
    val recyclerView: RecyclerView = findViewById(R.id.recyclerView)
    recyclerView.layoutManager = LinearLayoutManager(this)
    val adapter = RecyclerViewAdapter(dataList, listener)
    recyclerView.adapter = adapter
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    通过这种方式,可以减少监听对象的创建时间,提高性能,并且使用 holder.adapterPosition属性获取准确的 ID 或 Tag 进行判断。

    总结

    本篇文章,记录了我在项目中对RecyclerView的优化调研,和实际的优化手段。
    大家收藏备用哦!!!!!!

  • 相关阅读:
    vue之路由、无痕浏览加Nodejs环境安装及elementui介绍
    【博学谷学习记录】超强总结,用心分享|架构师-Kafka优化手段
    一张张截图教你使用gitee
    MySQL的replace into 与insert into on duplicate key update
    Python Flask 使用SQLAlchemy实现ORM管理
    Hamiton图系列文章 (5) :Hamilton图判定充要条件实现的算法复杂度分析
    细胞膜杂化脂质体载紫杉醇靶向/仿生型细胞膜嵌合脂质体递送KGF-2研究
    量子力学的应用:量子计算
    软件测试学习(四)自动测试和测试工具、缺陷轰炸、外包测试、计划测试工作、编写和跟踪测试用例
    ubuntu 20.04+ORB_SLAM3 安装配库教程
  • 原文地址:https://blog.csdn.net/weixin_45112340/article/details/132766481