记录一次项目中的《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) {
this@RvAdapter.oldList = oldList
this@RvAdapter.newList = 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 {
this@RvAdapter.oldList = oldList
this@RvAdapter.newList = 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的优化调研,和实际的优化手段。
大家收藏备用哦!!!!!!