Android UI篇之RecyclerView(四)

1,459 阅读4分钟

RecyclerView

RecyclerView是列表控件,用于展示大量的数据。不过会随着数据的增加,RecyclerView性能会受到影响,导致卡顿、内存泄漏等

简单使用

mBinding.recyclerView.layoutManager = LinearLayoutManager(context)
val adapter = ItemAdapter(context, data) { title ->
}
mBinding.recyclerView.adapter = adapter

// Adapter
class ItemAdapter(private val context: Context, private var data: List<String>,
    private val click: (title: String) -> Unit): RecyclerView.Adapter<ItemAdapter.Holder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder =
        Holder(ItemBinding.inflate(LayoutInflater.from(context), parent, false))

    override fun onBindViewHolder(holder: OcrHolder, position: Int) {
        val title = data[position]
        holder.binding.ocrPropTitle.text = title
        holder.binding.root.setOnClickListener {
            click(title)
        }
    }

    override fun getItemCount(): Int = data.size

    @SuppressLint("NotifyDataSetChanged")
    fun submit(list: List<String>) {
        data = list
        notifyDataSetChanged()
    }

    inner class Holder(val binding: ItemBinding): RecyclerView.ViewHolder(binding.root)
}

优化

  • 布局优化:减少嵌套 ConstraintLayoutmerge标签
  • 减少绘制:差异化刷新 notifyItemChanged(int position)DiffUtil
  • 滑动优化:滑动时暂停耗时操作 例glide取消加载图片
  • 预加载:预加载即将显示item,提高展示性能
  • 内存优化:及时释放内存,避免内存泄漏

布局优化

  • 减少嵌套层级 复杂布局合理使用ConstraintLayoutmerge标签合并布局
  • RecyclerView.setHasFixedSize(true) 固定item高度

item高度不变的列表,设置后不会因item改变触发重新计算,避免requestLayout导致的资源浪费

绘制优化

  • 分页 减少每页加载数据量
  • 局部刷新 只更新变化列表项notifyItemChanged(int position)
  • DiffUtil差异性处理 DiffUtil高效计算数据差异,并将结果用到RecyclerView中
val Diff(private val old: ArrayList<String>, private val new: ArrayList<String>): DiffUtil.Callback() {
    override fun getOldListSize() = old.size

    override fun getNewListSize() = new.size

    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
        old[oldItemPosition] == new[newItemPosition]

    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
        old[oldItemPosition] == new[newItemPosition]
}

val diff = DiffUtil.calculateDiff(Diff(old, new))
diff.dispatchUpdatesTo(adapter)

滑动优化

  • onCreateViewHolder()初始化 设置点击事件
  • onBindViewHolder()绑定数据 避免耗时操作
  • 添加滑动监听 RecyclerView.addOnScrollListener(listener)
recyclerView.addOnScrollListener(object: RecyclerView.OnScrollListener() {
    override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
        super.onScrollStateChanged(recyclerView, newState)
        if (newState == RecyclerView.SCROLL_STATE_IDLE) {
            // 空闲
            loaidng()
        } else {
            // 非空闲状态,例Glide.with(fragment).clear(holder.iv)
            pause()
        }
    }
})

预加载

  • calculateExtraLayoutSpace 预留额外空间,提前加载屏幕外Item,避免滑动中卡顿
  • collectAdjacentPrefetchPositions 滑动中预取相邻item数据,提高滑动流畅度
class LayoutManager(context: Context, orientation: Int = 1, reverseLayout: Boolean = false):
    LinearLayoutManager(context, orientation, reverseLayout) {
    constructor(context: Context) : super(context, 1)

    override fun calculateExtraLayoutSpace(state: RecyclerView.State, extraLayoutSpace: IntArray) {
        super.calculateExtraLayoutSpace(state, extraLayoutSpace)
        // 设置额外的布局空间,可以根据需要动态计算
        extraLayoutSpace[0] = 300 
        extraLayoutSpace[1] = 300
    }
    
    override fun collectAdjacentPrefetchPositions(dx: Int, dy: Int, state: RecyclerView.State?,
        layoutPrefetchRegistry: LayoutPrefetchRegistry) {
        super.collectAdjacentPrefetchPositions(dx, dy, state, layoutPrefetchRegistry)

        // 根据滑动方向(dx, dy)收集相邻的预取位置
        val anchorPos = findFirstVisibleItemPosition()
        if (dy > 0) {
            // 下滑,预取下面的Item数据
            for (i in anchorPos + 1 until state?.itemCount ?: 0) {
                layoutPrefetchRegistry.addPosition(i, 0)
            }
        } else {
            // 上滑,预取上面的Item数据
            for (i in anchorPos - 1 downTo 0) {
                layoutPrefetchRegistry.addPosition(i, 0)
            }
        }
    }
}

内存优化

  • 共用RecyclerViewPool 适用于多个RecyclerView之间的数据或布局结构相似的场景
val pool = RecyclerView.RecycledViewPool()
recycler1.setRecycledViewPool(pool)
recycler2.setRecycledViewPool(pool)
  • adapter.setHasStableIds(true) 提高复用ViewHolder稳定性,减少内存消耗
  • recyclerView.setItemViewCacheSize(30) 设置RV中ViewHolder缓存数量
  • 重写Adapter#onViewRecycled(holder) 资源回收,避免内存泄漏和资源浪费
override fun onViewRecycled(holder: ViewHolder) {
    super.onViewRecycled(holder)

    // 释放图片资源
    holder.binding.iv.setImageDrawable(null)

    // 移除监听器
    holder.binding.root.setOnClickListener(null)
}

缓存机制

为提高列表滚动时的性能,而采用多级缓存策略来存储和复用View,减少View的创建和销毁,进而减少内存分配和GC触发频率

Recycler

  • 负责回收和复用ViewHolder的类
class Recycler {
    // 存放可视范围内的ViewHolder(但在onLayoutChildren的时,会将所有View都缓存到这),复用时若position或id对应上,则无需重新绑定数据
    val mAttachedScrap: ArrayList<ViewHolder> = ArrayList()

    // 存放可视范围内数据发生变化的ViewHolder,复用时需重新绑定数据
    var mChangedScrap: ArrayList<ViewHolder>? = null

    // 存放remove的ViewHolder,复用时若position或id对应上,则无需重新绑定数据
    val mCachedViews: ArrayList<ViewHolder> = ArrayList()

    // 默认值为2
    private var mRequestedCacheMax: Int = DEFAULT_CACHE_SIZE

    // 默认值为2
    var mViewCacheMax: Int = DEFAULT_CACHE_SIZE

    // 存放remove && 重置数据的ViewHolder,复用时需要重新绑定数据。 默认大小是 5 
    var mRecyclerPool: RecycledViewPool? = null

    // 自定义的缓存
    private var mViewCacheExtension: ViewCacheExtension? = null
}

一级缓存(Scrap缓存)

  • 屏内缓存,包括mAttachedScrapmChangedScrap,用于保存屏内可视或即将可视的ViewHolder
  • mAttachedScrap:存放已加入RecyclerView但与RecyclerView临时分离的ViewHolder(例滚动中场景)
  • mChangedScrap:存放数据已改变但尚未重新绑定数据的ViewHolder(例动画播放场景)

二级缓存(Cache缓存)

  • 离屏缓存,mCachedViews,用于保存尚未被回收的ViewHolder
  • 大小有限制,默认大小为2
  • 当需展示新视图时,会先检查Cache缓存中是否有可用的ViewHolder

三级缓存(ViewCacheExtension)

  • 自定义缓存,由开发者自定义策略,按需缓存更多的ViewHolder
  • 通过实现ViewCacheExtension接口扩展

终极缓存(RecycledViewPool)

  • 回收缓存池,mRecyclerPool,用于存放被标记为废弃的ViewHolder(其他缓存不需要的ViewHolder)
  • 池中ViewHolder已被抹除数据,使用时需重新绑定数据
  • RecycledViewPool会根据itemType创建不同的集合存储ViewHolder

策略

  1. RecyclerView滚动时,先移除滑出屏幕的item,并将ViewHolder缓存至mCachedViews中,若mCachedViews(有大小限制)已满,会将最早加入的ViewHolder移除并存入RecycledViewPool中。复用时逐级从缓存中获取ViewHolder,不为null复用,为null创建新的ViewHolder
  2. 当某个item数据发生变化时,若ViewHolder是可视的,则会被缓存至mChangedScrap中,需重新绑定数据时,直接从缓存中获取该ViewHolder
  3. 删除item时,其对应的ViewHolder会先后进入Scrap缓存、Cached缓存、RecycledViewPool