RecyclerView.ViewCacheExtension 使用及踩坑
Posted 涂程
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了RecyclerView.ViewCacheExtension 使用及踩坑相关的知识,希望对你有一定的参考价值。
作者:Delicia_Lani
前言
最近遇到一个需求,需求实现上并不复杂,大概长这个样:
基本上就是一个RecyclerView 嵌套多个子 RecyclerView ,有横向的,也有竖向的。RecyclerView 实现多类型布局有各种各样的实现方式,这里就不多说了。
本来很开心的实现完了,在测试中确遇到了非常严重的性能问题,也就了本篇文章的诞生。具体的讲,嵌套的横向滑动的RecyclerView 没有任何问题,嵌套的竖向RecyclerView 在上下滑动时却遇到了非常严重的性能问题,表现为在从自上向下滑动到显示竖向的RecyclerView 时,在条目比较多时会感受到非常明显的卡顿。
那为啥竖向的条目为啥还要嵌套一个RecyclerView,直接显示在RecyclerView 中就好了呀,嗯我也想这样子实现。都说设计有三宝,卡片、阴影、圆角,这里就卡到了卡片问题上,设计需求中竖向的RecyclerView外带一个卡片样式,这就要求须用一个卡片样式的布局包一下竖向的条目(尝试使用ItemDecoration 尝试自己绘制,遇到各种问题,遂放弃)。因为这个竖向嵌套的RecyclerView 作为主RecyclerView的一个Item,在显示到屏幕的过程中所有子Item都需要完整的测量,布局和绘制,如果嵌套的竖向RecyclerView中Item过多,这些Item即使没有显示在主RecyclerView中,也需要经过完整的测量和布局,进而计算出竖向嵌套的RecyclerView 的尺寸,这就时导致卡顿的根源。
既然知道了卡顿的原因,要怎么解决呢,在与设计PK 改方案无果后(其实主要是ios实现了),开始默默调研起解决方案,首先肯定是要从RecyclerView的缓存入手,首先尝试简单的解决方案。
尝试使用简单的优化方案
//竖向嵌套RecyclerView,加快测量
recyclerView.setHasFixedSize(true)
//给定足够的缓存数,减少竖向recyclerView 中item 的重建
recyclerView.recycledViewPool.setMaxRecycledViews(viewType, 20)
应用简单方案后,性能仍然不满足需求,继续啃源码查调研方案,在RecycledViewPool
中没有看到可以介入的逻辑,看到了ViewCacheExtension
可以拦截Recycler的获取视图的逻辑。也调研了网上的使用方案,发现基本没有使用RecyclerView.ViewCacheExtension
的记录,遂开始踩坑。
尝试使用ViewCacheExtension
介入RecyclerView缓存
实现一个ViewCacheExtension
简单了解后,ViewCacheExtension
会介入RecyclerView 的缓存逻辑,Recycler 会早于RecyclerViewPoll 在 ViewCacheExtension 查找是否有对应视图,这就提供给我们时机介入缓存逻辑。 实现一个自己ViewCacheExtension
很简单,只需要复写一个getViewForPositionAndType
方法在合适的时机返回自己管理的View就好了。
class SingleCacheEx(private val interestedType: IntArray) : RecyclerView.ViewCacheExtension()
override fun getViewForPositionAndType(
recycler: RecyclerView.Recycler,
position: Int,
viewType: Int
): View?
//返回自己管理的视图
如何介入到RecyclerView的缓存
1.接入RecyclerView
//RecyclerView 接入
val types = intArrayOf(TYPE_ONE,TYPE_TWO)
val singleCacheEx = SingleCacheEx(types)
//接入ViewCacheExtension
recyclerView.setViewCacheExtension(singleTypeCacheEx)
//同时设置RecycledViewPool中已有ViewCacheExtension接管的Type最大缓存数为 0
val poll = recyclerView.recycledViewPool
for(type in types) poll.setMaxRecycledViews(type,0)
2.介入缓存 从源码中我们看到 ViewCacheExtension#getViewForPositionAndType
需要返回一个view 给RecyclerView,那么这个View从哪里来呢?
既然在上面我们设置了特定类型的viewHolder不会在 recycledViewPool 缓存,那么在 Adapter 的 onCreateViewHolder
中介入就是一种可行的方案
override fun onCreateViewHolder(container: ViewGroup, viewType: Int): VH
val item = LayoutInflater.from(container.context)
.inflate(R.layout.card_module_item_layout_main_page, container, false)
return VH(item).apply
//讲viewHolder 保存到 singleCacheEx 中
singleCacheEx.onCreateViewHolder(viewType, this)
将其保存到SingleCacheEx
的 map 字段中,这样子在Recycler 调用 getViewForPositionAndType 获取视图时,我们就可以查找缓存的View返回给其使用。主动管理了缓存,就需要承担内存泄露的风险,我们也需要在和合适的时机清空缓存,方式Context 泄露,因此还需要实现相应的clear逻辑
class SingleCacheEx(private val interestedType: IntArray) : RecyclerView.ViewCacheExtension()
private val singleVHMap = mutableMapOf<Int, RecyclerView.ViewHolder>()
override fun getViewForPositionAndType(
recycler: RecyclerView.Recycler,
position: Int,
viewType: Int
): View?
if (viewType !in interestedType)
return null
val vh = singleVHMap[viewType] ?: return null
return if (isStateSafe(recycler, vh))
vh.itemView
else
singleVHMap.remove(viewType)
null
fun onCreateViewHolder(viewType: Int, vh: RecyclerView.ViewHolder)
if (viewType in interestedType)
singleVHMap[viewType] = vh
private fun isStateSafe(
recycler: RecyclerView.Recycler,
viewHolder: RecyclerView.ViewHolder
) :Boolean
return viewHolder.itemView.parent == null
fun clear()
if (singleVHMap.isNotEmpty())
singleVHMap.clear()
fun clearType(type: Int)
singleVHMap.remove(type)
遇到的一些坑
本来文章到这里就可以结束了,但是作为一个多端App,在适配平板和折叠屏时又遇到了一些新的坑。
OnConfigurationChange
适配的坑
如果缓存的视图不可见时,屏幕发生了OnConfigurationChange
事件,这时缓存的View脱离了视图树,无法收到这个事件,也就无法响应相关改变
getViewForPositionAndType
获取到view 后并不会走onBindViewHolder
的逻辑,RecyclerView 认为你这个View 不是Dirty的,不需要重新绑定。
兜兜转转一圈,想到我们可以在 OnConfigurationChange
发生后主动给SingleCacheEx缓存的但目前没有在布局树中的View设置一个tag,在重新添加到布局树中时检查这个tag即可。
需要监听 OnConfigurationChange
事件的ViewHolder
实现 ConfigurationChangeAware
接口
class ViewHolder(item):RecyclerView.ViewHolder(item):OnConfigurationChange
fun notifyConfigurationChange()
//post 到AttachInfo.mHandler中,这样子才会在attach后执行
post
// do configuration change
SingleTypeCacheEx
中修改的方法如下
class SingleTypeCacheEx(private val interestedType: IntArray) : RecyclerView.ViewCacheExtension()
interface ConfigurationChangeAware
fun notifyConfigurationChange()
override fun getViewForPositionAndType(
recycler: RecyclerView.Recycler,
position: Int,
viewType: Int
): View?
if (viewType !in interestedType)
return null
val vh = singleVHMap[viewType] ?: return null
return if (isStateSafe(recycler, vh))
dispatchLazyConfigurationChangeIfNeeded(vh)
vh.itemView
else
singleVHMap.remove(viewType)
null
private fun dispatchLazyConfigurationChangeIfNeeded(vh: RecyclerView.ViewHolder)
if (isConfigurationChanged(vh))
setConfigurationChanged(vh, false)
if (vh is ConfigurationChangeAware)
vh.notifyConfigurationChange()
fun dispatchOnConfiguration()
for ((_, vh) in singleVHMap)
val isDetached = vh.itemView.parent == null
if (isDetached)
setConfigurationChanged(vh, true)
private fun isConfigurationChanged(vh: RecyclerView.ViewHolder): Boolean
return vh.itemView.getTag(R.id.single_cache_extension_configuration_change_flag) as? Boolean == true
private fun setConfigurationChanged(vh: RecyclerView.ViewHolder, value:Boolean)
vh.itemView.setTag(R.id.single_cache_extension_configuration_change_flag, value)
ItemDecoration
的坑RecyclerView#LayoutParams
中有一个字段标志来支持是否调用ItemDecoration#getItemOffsets
更新Offset,但是在我们手动管理缓存时Offset 并不总是会刷新,这个解决方案也很多,比如可以在getViewForPositionAndType
方式反射mInsetsDirty
设置为true 在attach 到布局后就会自动刷新;或者仿照方案一在attach后手动设置一下ItemDecoration
触发更新 offset
public static class LayoutParams extends android.view.ViewGroup.MarginLayoutParams
boolean mInsetsDirty = true;
...
总结
使用ViewCacheExtension还是比较危险的,对内存也会造成一定的压力,在使用时需要合理考虑实现成本与收益。
以上是关于RecyclerView.ViewCacheExtension 使用及踩坑的主要内容,如果未能解决你的问题,请参考以下文章