RecyclerView缓存之ViewCacheExtension

场景

最近在开发产品的过程中,有这样一个场景:页面包含一个竖向列表,当点击特定的item时会在其中展示建议视图。这些建议视图从服务器获取并展示在一个横向滑动的列表中。我们还是如常使用RecyclerView去做。

技术规划

现在让我们深入到技术细节,获取建议并展示的逻辑是在一个单独的view holder中处理,其内部包含它自己的RecyclerView去展示横向的列表。在onBindView中,这个view holder执行获取数据和在RecyclerView中展示的逻辑。我们不能在不知道用户点击具体哪个item就去提前获取数据,而且我们也不能把数据保存到父级adapter中,就结构层面来说我们想要保证这些view holder逻辑是分离的。

出现的问题

当我们每次滑动列表到这个view holder显示在屏幕中时,onBind会被调用并且我们不得不重新获取数据并配置新的adapter然后展示到UI上。

解决方案

如果我们能禁止这个建议view holder每次都调用onBind就没问题了 。为了达到这个目的,我们需要做两件事。首先,我们需要告诉RecyclerView始终重用同一个view holder,同一位置不再调用onBind.其次,相同的view Type在不同的位置上不能使用这个view holder。

简而言之,我们需要一个可以缓存特定类型视图的解决方案。

不太妥帖的方案

1.我们可以增加RecyclerView的缓存数量。但是,这样的话我们就必须把缓存数量设置成跟整个列表的数量一致,这是因为RecyclerView只能缓存我们可见区域内的视图,所以如果你的view holder远离可见区域它将不会被缓存,直到缓存大小等于整个列表。

但是,这种方案对内存是不友好的,当你将整个列表都缓存起来的话,使用RecyclerView也就没有意义了。

2.其次,有人会建议你去将特定的view holder定义成不可复用。

holder.setIsRecyclable(false);

或是

recyclerView.getRecycledViewPool()
            .setMaxRecycledViews(TARGET_VIEW_HOLDER, POOL_CAPACITY);
//即设置 POOL_CAPACITY 为 0.

它将帮助你实现第二点即对相同的类型且不同位置不去使用view holder,不会将它移动到RecyclerViewPool。但是,它不会在所有情况下都满足我们的目的,因为,当我们将目标view holder从可见区域移开时,RecyclerView仍然会从cache中清除它。它只是没有移到RecyclerViewPool中。

进阶方案

RecyclerView提供了一个回调方法:

ViewCacheExtension()

RecyclerView在检查了它自身的cache或scrap view之后以及在将其移动到RecyclerViewPool之前会调用它的方法 getViewForPositionAndType 。这是一个抽象类,你可以实现它并返回特定位置的视图。

val viewCacheExtension = object : ViewCacheExtension() {
  override fun getViewForPositionAndType(
        recycler: Recycler,
        position: Int,
        type: Int
    ): View? {
       
    }
}

我们可以利用它来缓存特定的视图类型。

让我们看看如何使用它来达到我们的目的,并创建一个通用的解决方案,使它允许为特定的视图类型启用缓存。

1.选取特定的view holder去缓存

我们需要一个存储view holder的Map用来保存这些已经缓存的视图。我们还需要存储需要启用缓存的视图holder类型,这里我们使用Set。

val cachedItems: MutableMap<Long, ViewHolder> = HashMap()  
//这里的Key是唯一的用来标识特定位置的item
val cachedViewHolderTypes: MutableSet<Int> = HashSet()

现在填充已经创建的视图的最佳时机是:

onViewDetachedFromWindow()

这是因为视图已经被创建并且已经被绑定了一次,在它被回收之前,我们将它保存到我们的Map中:

 //一旦视图划出屏幕便将其保存起来
    override fun onViewDetachedFromWindow(holder: ViewHolder) {
        if (cachedViewHolderTypes.contains(holder.itemViewType)) {
            val pos = holder.bindingAdapterPosition
            Timber.d("onViewDetached called for position : $pos")
            if (pos > -1) {
                holder.setIsRecyclable(false)
                cachedItems[getCachedItemId(pos)] = holder
            }

        }
        super.onViewDetachedFromWindow(holder)
    }

这里,我们存储的view holder与其Item Id相关联,即RecyclerView的getItemId(int pos)方法。

可能你也注意到了我们将这个holder置为可被回收,否则相同的holder将被我们重用到其他的位置上,因为一旦划出屏幕RecyclerView便将其移动到RecyclerView Pool中。

2.在onBindView调用之前返回已缓存的view holder

对此,我们像这样使用ViewCacheExtension:

//返回特定位置已缓存的view holder
    private val viewCacheExtension = object : ViewCacheExtension() {
        override fun getViewForPositionAndType(
            recycler: Recycler,
            position: Int,
            type: Int
        ): View? {
            val cachedItemId = getItemId(position)
            if (cachedViewHolderTypes.contains(type) && cachedItemId != NO_CACHED_ITEM_ID
                && cachedItems.containsKey(cachedItemId)
            ) {
                Timber.d("Returning view from custom cache for position:$position")
                return cachedItems[cachedItemId]?.itemView
            }
            return null
        }
    }

在返回已缓存的视图之前需要做相关的检查:

  1. 通过对集合进行查询,我们检查对应的view holder类型是否已启用缓存。
  2. 是否这个holder的item id 和当前指向的位置相同。这是为了确保在我们的adapter已经被新的数据更新了但是我们的缓存Map还持有旧的view holder的场景不会发生。这是重要的,因为在数据集更新后,新的视图类型被分配到这个位置上,但是,我们仍然为这个位置返回旧的view holder,这将导致仍显示旧的视图。

3.确保在数据集变更的同时缓存的视图也会被更新

为了达到当数据集有变化,我们就能更新缓存Map的目的,我们需要利用AdapterDataObserver 观察数据的变化。这样当有任何通知到来我们会得到回调。

我们可以将数据集变更调用主要分为两组来处理:

  1. 使用notifyItemRangeRemoved()移除的缓存项。
  2. 我们在notifyItemRangeMoved()或notifyDataSetChanged()之后移除的view holder。

为了简化这个数据集变化导致的分组,我们继承AdapterDataObserver类来实现一个抽象类。

这里包含两个回调方法

  1. onChanged() : 我们观察任何数据集的变化除了数据被移除这个操作外。所以,我们只是检查,如果我们的缓存项是否仍然存在于新的集合中。

如果缓存项不在新的集合中,这个观察器将负责删除它们。

override fun onChanged() {
            Timber.d("items added or moved")
            if (cachedItems.isEmpty())
                return

            val iterator: MutableIterator<String> = cachedItems.keys.iterator()

            val newItemsIds = mutableSetOf<String>()

            for (newIndex in 0 until itemCount) {
                val itemId = getItemId(newIndex)
                newItemsIds.add(itemId)
            }

            //移除新集合中没有的缓存项
            while (iterator.hasNext()) {
                val cachedItemId = iterator.next()
                val holder = cachedItems[cachedItemId]
                if (holder?.bindingAdapterPosition == -1 || !newItemsIds.contains(cachedItemId)) {
                    holder?.itemView?.let { view ->
                        if (view.isAttachedToWindow || view.parent != null) {
                            recyclerView?.removeView(view)
                            view.visibility = View.GONE
                        }
                    }
                    iterator.remove()
                }
            }
        }
  1. onItemRangeRemoved(positionStart: Int, itemCount: Int) :这里,我们观察数据的删除操作。

 override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
            Timber.d("On Item range removed  called for adapter observer")
            if (cachedItems.isEmpty())
                return
            val iterator: MutableIterator<String> = cachedItems.keys.iterator()

            while (iterator.hasNext()) {
                val cachedItemId = iterator.next()
                val index = cachedItems[cachedItemId]?.bindingAdapterPosition ?: -1
                if (index != -1 && positionStart <= index && index < positionStart + itemCount) {
                    Timber.d("Removing view from adapter observer for position:$index")
                    val holder = cachedItems[cachedItemId]
                    holder?.itemView?.let { view ->
                        if (view.isAttachedToWindow || view.parent != null) {
                            recyclerView?.removeView(view)
                            view.visibility = View.GONE
                        }
                    }
                    iterator.remove()
                }
            }
        }

4.在RecyclerView中注册这些回调方法

override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
    super.onAttachedToRecyclerView(recyclerView)
    registerAdapterDataObserver(adapterDataObserver)
    recyclerView.setViewCacheExtension(viewCacheExtension)
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
    unregisterAdapterDataObserver(adapterDataObserver)
    super.onDetachedFromRecyclerView(recyclerView)
}

5.一个简化的方法去为特定的view holder类型启动缓存

fun enableCacheForViewHolderType(type: Int) {
    cachedViewHolderTypes.add(type)
}

到这里我们已经准备好了,我们自己实现的为RecyclerView特定view holder类型进行缓存的逻辑,只需要在onCreateViewHolder ()加入一行代码即可。

enableCacheForViewHolderType(viewType);

现在,在被声明view holder类型的onBindView方法只会执行一次。

完整代码:

import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.*
import timber.log.Timber
import java.util.HashSet
import java.util.concurrent.ConcurrentHashMap

/**
 * Created by Sumeet on 24,January,2022
This class helps you to return same view holder once bind , without rebinding again
Just sent the required type for enabling cache using enableCacheForViewHolderType and overriding getItemId()
*/
abstract class BaseAdapterWithCaching : RecyclerView.Adapter<ViewHolder>() {

    private val cachedItems: MutableMap<String, ViewHolder> = ConcurrentHashMap()
    private val cachedViewHolderTypes: MutableSet<Int> = HashSet()

    companion object {
        const val NO_CACHED_ITEM_ID = "NO_CACHED_ITEM_ID"
    }

    //Return cached view holder for particular position
    private val viewCacheExtension = object : ViewCacheExtension() {
        override fun getViewForPositionAndType(
            recycler: Recycler,
            position: Int,
            type: Int
        ): View? {
            val cachedItemId = getItemId(position)
            if (cachedViewHolderTypes.contains(type) && cachedItemId != NO_CACHED_ITEM_ID
                && cachedItems.containsKey(cachedItemId)
            ) {
                Timber.d("Returning view from custom cache for position:$position")
                return cachedItems[cachedItemId]?.itemView
            }
            return null
        }
    }

    fun enableCacheForViewHolderType(type: Int) {
        cachedViewHolderTypes.add(type)
    }

    protected fun isCacheEnabledForViewHolder(type: Int): Boolean =
        cachedViewHolderTypes.contains(type)

    private val adapterDataObserver: DataChangeObserver = object :
        DataChangeObserver() {   //This observers will take care of removing the cached items if they are not in new set
        override fun onChanged() {
            Timber.d("items added or moved")
            if (cachedItems.isEmpty())
                return

            val iterator: MutableIterator<String> = cachedItems.keys.iterator()

            val newItemsIds = mutableSetOf<String>()

            for (newIndex in 0 until itemCount) {
                val itemId = getItemId(newIndex)
                newItemsIds.add(itemId)
            }

            //Remove cached items which are not in new set
            while (iterator.hasNext()) {
                val cachedItemId = iterator.next()
                val holder = cachedItems[cachedItemId]
                if (holder?.bindingAdapterPosition == -1 || !newItemsIds.contains(cachedItemId)) {
                    holder?.itemView?.let { view ->
                        if (view.isAttachedToWindow || view.parent != null) {
                            recyclerView?.removeView(view)
                            view.visibility = View.GONE
                        }
                    }
                    iterator.remove()
                }
            }
        }

        override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
            Timber.d("On Item range removed  called for adapter observer")
            if (cachedItems.isEmpty())
                return
            val iterator: MutableIterator<String> = cachedItems.keys.iterator()

            while (iterator.hasNext()) {
                val cachedItemId = iterator.next()
                val index = cachedItems[cachedItemId]?.bindingAdapterPosition ?: -1
                if (index != -1 && positionStart <= index && index < positionStart + itemCount) {
                    Timber.d("Removing view from adapter observer for position:$index")
                    val holder = cachedItems[cachedItemId]
                    holder?.itemView?.let { view ->
                        if (view.isAttachedToWindow || view.parent != null) {
                            recyclerView?.removeView(view)
                            view.visibility = View.GONE
                        }
                    }
                    iterator.remove()
                }
            }
        }
    }

    override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
        super.onAttachedToRecyclerView(recyclerView)
        registerAdapterDataObserver(adapterDataObserver)
        recyclerView.setViewCacheExtension(viewCacheExtension)
    }

    override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
        Timber.d("onDetached recycler view called ")
        unregisterAdapterDataObserver(adapterDataObserver)
        super.onDetachedFromRecyclerView(recyclerView)
    }

    //Saving the cached item once it goes out of screen
    override fun onViewDetachedFromWindow(holder: ViewHolder) {
        if (cachedViewHolderTypes.contains(holder.itemViewType)) {
            val pos = holder.bindingAdapterPosition
            Timber.d("onViewDetached called for position : $pos")
            if (pos > -1) {
                holder.setIsRecyclable(false)
                cachedItems[getItemId(pos)] = holder
            }

        }
        super.onViewDetachedFromWindow(holder)
    }

    private abstract class DataChangeObserver : AdapterDataObserver() {
        abstract override fun onChanged()
        override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
            onChanged()
        }

        override fun onItemRangeChanged(
            positionStart: Int, itemCount: Int,
            payload: Any?
        ) {
            onChanged()
        }

        override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
            onChanged()
        }

        override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
            onChanged()
        }

        override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
            onChanged()
        }
    }

}
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容