看这篇文章可以让你了解到:在一个复杂的RecyclerView中,有数百个Item,每个Item都包含大量的数据和图像。如何有效地加载和显示这些数据,同时保持列表的平滑滚动?
在一个在线购物APP上遇到的问题(不是多点APP),有数百个商品的展示的Item,每个Item都有大量的数据和图片展示。也就是数据量很大,导致商品列表加载和显示过程很慢。
DiffUtil 是一个用于计算两个列表之间差异的实用工具类。它通过比较两个列表的元素,找出它们之间的差异,并生成更新操作的列表,以便进行最小化的更新操作。
当然这种最小化的更新操作完全可以通过严格的去使用notify相关的API去控制,所以我认为DiffUtil是一种最小化更新操作的规范形式。(ps:毕竟难免的会错误的触发notify导致资源的浪费)
注意:强调的是DiffUtil的更新,如果只是单独的添加还是希望去用notifyItemInserted(),单独的添加的操作在业务中你肯定是知道的。
原理:了解一下就可以了:DiffUtil 使用最长公共子序列(Longest Common Subsequence,LCS)算法来比较两个数据集之间的差异。算法首先创建会一个矩阵,矩阵的行表示旧数据集的元素,列表示新数据集的元素。之后通过回溯构建最长公共子序列,通过比较不属于最长公共子序列的元素,来确定两个数据集之间的差异。
我们在使用DiffUtil也是主要去使用DiffUtil.Callback,他掌握着重要的监控差异性的几个抽象方法。
具体的代码和解释如下:
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)做了什么吧。差异性的算法刚才上面说了一嘴
// 将比对结果应用到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过程进行异步处理,是有必要做的。(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)
}
}
修改后的代码
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)
}
}
}
如果你用的都是不可变的对象,也就是Final修饰的那就没问题。
如果是可变对象,那么你要重写equals和hashCode方法以便DiffUtil正确比较数据项,具体代码按实际业务来。
当然分页加载数据是必须项:关于列表的内容,都需要由服务器返回的分页数据。这样避免了一次性加载过度数据带来的请求延迟。也减轻了服务器的压力。
那我们要怎么优化这个分页呢?
既然预加载作为我们优化加载速度重要的一个思想。那么在分页中是不是也可以加入这个思想呢?
也就是说:在一页数据还未看完时就请求下一页数据。那么我们可以通过两种思想去做:
两种方法都是提前加载下一页的数据,来进行优化用户的感知。
我们这里只说一下第一种方式,第二种方式是类似的。
class PreloadAdapter : RecyclerView.Adapter<ViewHolder>() {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
checkPreload(position) // 判断是否达到阈值
}
注意:这里对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)
}
})
}
// 增加预加载状态标记位
var isPreloading = false
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()
}
}
}
val preloadAdapter = PreloadAdapter().apply {
// 在距离列表尾部还有2个表项的时候预加载
preloadItemCount = 2
onPreload = {
// 预加载业务逻辑
}
}
我们可以利用自定义公共的监听来减少监听对象的创建时间,提高性能,并且使用 holder.getAdapterPosition() 方法获取准确的 ID 或 Tag 进行判断。
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.itemText.text = mItemList[position]
holder.itemText.setOnClickListener({
// 具体点击业务
})
}
这样会在每次绑定View,也就是执行onBindViewHolder都去对itemText设置监听对象,这样大量的频繁的创建对象,你这是要干嘛!!!!
interface RecyclerViewListener {
fun onItemClick(position: Int)
fun onItemLongClick(position: Int)
}
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
}
}
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
}
}
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的优化调研,和实际的优化手段。
大家收藏备用哦!!!!!!