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

记录一次项目中的《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?
    • 分别获取新老列表中对应位置的元素,如果这两个元素相同,但是内容发生改变,可以通过这个方法获取它们之间的差异信息,从而只更新需要改变的部分,减少不必要的更新操作。

具体的代码和解释如下

kotlin 复制代码
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)
    }
    
    // 其他常规的函数
    .....
    .....
}

简单看一下源码

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

kotlin 复制代码
// 将比对结果应用到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);
    }
}

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

异步化Diff计算过程

上面说了,是通过算法进行计算,来统计我们的差异性。那当遇到大数据,难免的会遇到计算带来的耗时问题。

所以将这个Diff过程进行异步处理,是有必要做的。(ps:直接用协程得了)

kotlin 复制代码
suspend fun upDataList(oldList: MutableList<MessageData>, newList: MutableList<MessageData>) =
    withContext(Dispatchers.Default) {
        [email protected] = oldList
        [email protected] = newList

        // 利用DiffUtil比对结果
        val diffResult = DiffUtil.calculateDiff(diffUtilCallBack)
        withContext(Dispatchers.Main) {
            // 将比对结果应用到 adapter
            diffResult.dispatchUpdatesTo(this@RvAdapter)
        }
    }

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

修改后的代码

kotlin 复制代码
private val updateListMutex = Mutex()

suspend fun upDataList(oldList: MutableList<MessageData>, newList: MutableList<MessageData>) = withContext(Dispatchers.Default) {
    // 加锁,保护数据的访问和修改
    updateListMutex.withLock {
        [email protected] = oldList
        [email protected] = newList
    }

    // 利用DiffUtil比对结果
    val diffResult = DiffUtil.calculateDiff(diffUtilCallBack)

    withContext(Dispatchers.Main) {
        // 加锁,保护数据的访问和修改
        updateListMutex.withLock {
            // 将比对结果应用到 adapter
            diffResult.dispatchUpdatesTo(this@RvAdapter)
        }
    }
}
  • DiffUtil是通过比较两个数据对象的引用来判断它们是否相同的

如果你用的都是不可变的对象,也就是Final修饰的那就没问题。

如果是可变对象,那么你要重写equals和hashCode方法以便DiffUtil正确比较数据项,具体代码按实际业务来。

分页的预加载

优化的思路

当然分页加载数据是必须项:关于列表的内容,都需要由服务器返回的分页数据。这样避免了一次性加载过度数据带来的请求延迟。也减轻了服务器的压力。

那我们要怎么优化这个分页呢?

既然预加载作为我们优化加载速度重要的一个思想。那么在分页中是不是也可以加入这个思想呢?

也就是说:在一页数据还未看完时就请求下一页数据。那么我们可以通过两种思想去做:

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

实现(第一种)

两种方法都是提前加载下一页的数据,来进行优化用户的感知。

我们这里只说一下第一种方式,第二种方式是类似的。

  • 第一步:重写RecyclerView的Adapter监听列表的绑定Item的position,当达到阈值时去请求数据
kotlin 复制代码
class PreloadAdapter : RecyclerView.Adapter<ViewHolder>() {

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        checkPreload(position)    // 判断是否达到阈值
    }

注意:这里对RecyclerView了解的可能会问,那onBindViewHolder会在RecyclerView预加载的时候就会被回调。并不是当前Item显示在页面的时候。

答:当然,但是第一点你可以去设置RecyclerView预加载的个数,第二点如果预加载的时候就会被回调那么请求被提前了,有什么不好呢?

  • 第二步:监听滑动状态,当确定是滑动触发时再加载
kotlin 复制代码
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
    recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
        override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
            // 更新滚动状态
            scrollState = newState
            super.onScrollStateChanged(recyclerView, newState)
        }
    })
}
  • 第三步:防止列表滚动到底部触发了一次预加载后,又往回滚动。 再次滚下来,当预加载未完成,会再次触发的风险。
kotlin 复制代码
// 增加预加载状态标记位
var isPreloading = false
  • 完整代码
kotlin 复制代码
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()
        }
    }
}
  • 第四步:调用
kotlin 复制代码
val preloadAdapter = PreloadAdapter().apply {
    // 在距离列表尾部还有2个表项的时候预加载
    preloadItemCount = 2
    onPreload = {
        // 预加载业务逻辑
    }
}

使用公共监听

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

错误的做法

kotlin 复制代码
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    holder.itemText.text = mItemList[position]
    holder.itemText.setOnClickListener({ 
        // 具体点击业务
    })
}

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

建议的做法

  • 第一步:首先,在 RecyclerView 的适配器中定义一个接口,作为公用的监听器:
kotlin 复制代码
interface RecyclerViewListener {
    fun onItemClick(position: Int)
    fun onItemLongClick(position: Int)
}
  • 第二步:然后,在 RecyclerView 的 ViewHolder 中设置监听器:
kotlin 复制代码
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
    }
}
  • 第三步:接下来,在适配器中设置监听器:
kotlin 复制代码
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
    }
}
  • 第四步:最后,在使用 RecyclerView 的地方,设置公用的监听器并创建适配器:
kotlin 复制代码
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

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

总结

本篇文章,记录了我在项目中对RecyclerView的优化调研,和实际的优化手段。

大家收藏备用哦!!!!!!

相关推荐
韶博雅9 分钟前
mysql表类型查询
android·数据库·mysql
studyForMokey15 分钟前
【Android学习记录】工具使用
android·学习
小wanga26 分钟前
【MySQL】索引特性
android·数据库·mysql
好学人29 分钟前
Kotlin object 关键字详解
kotlin
好学人31 分钟前
Kotlin sealed 关键字介绍
kotlin
牛了爷爷1 小时前
php伪协议
android·开发语言·php
岸芷漫步1 小时前
Kotlin中的序列化应用
kotlin
鸿蒙布道师1 小时前
鸿蒙NEXT开发文件预览工具类(ArkTs)
android·ios·华为·harmonyos·arkts·鸿蒙系统·huawei
鸿蒙布道师1 小时前
鸿蒙NEXT开发全局上下文管理类(ArkTs)
android·ios·华为·harmonyos·arkts·鸿蒙系统·huawei
树獭非懒1 小时前
ContentProvider存在的意义:从Android沙箱机制看安全数据共享的设计哲学
android·客户端