RecyclerView緩存之ViewCacheExtension

場景

最近在開發(fā)產(chǎn)品的過程中,有這樣一個場景:頁面包含一個豎向列表钉赁,當(dāng)點擊特定的item時會在其中展示建議視圖昧互。這些建議視圖從服務(wù)器獲取并展示在一個橫向滑動的列表中荤西。我們還是如常使用RecyclerView去做。

技術(shù)規(guī)劃

現(xiàn)在讓我們深入到技術(shù)細節(jié)退敦,獲取建議并展示的邏輯是在一個單獨的view holder中處理粘咖,其內(nèi)部包含它自己的RecyclerView去展示橫向的列表。在onBindView中侈百,這個view holder執(zhí)行獲取數(shù)據(jù)和在RecyclerView中展示的邏輯瓮下。我們不能在不知道用戶點擊具體哪個item就去提前獲取數(shù)據(jù),而且我們也不能把數(shù)據(jù)保存到父級adapter中钝域,就結(jié)構(gòu)層面來說我們想要保證這些view holder邏輯是分離的讽坏。

出現(xiàn)的問題

當(dāng)我們每次滑動列表到這個view holder顯示在屏幕中時,onBind會被調(diào)用并且我們不得不重新獲取數(shù)據(jù)并配置新的adapter然后展示到UI上例证。

解決方案

如果我們能禁止這個建議view holder每次都調(diào)用onBind就沒問題了 路呜。為了達到這個目的,我們需要做兩件事织咧。首先拣宰,我們需要告訴RecyclerView始終重用同一個view holder,同一位置不再調(diào)用onBind.其次烦感,相同的view Type在不同的位置上不能使用這個view holder巡社。

簡而言之,我們需要一個可以緩存特定類型視圖的解決方案手趣。

不太妥帖的方案

1.我們可以增加RecyclerView的緩存數(shù)量晌该。但是肥荔,這樣的話我們就必須把緩存數(shù)量設(shè)置成跟整個列表的數(shù)量一致,這是因為RecyclerView只能緩存我們可見區(qū)域內(nèi)的視圖朝群,所以如果你的view holder遠離可見區(qū)域它將不會被緩存燕耿,直到緩存大小等于整個列表。

但是姜胖,這種方案對內(nèi)存是不友好的誉帅,當(dāng)你將整個列表都緩存起來的話,使用RecyclerView也就沒有意義了右莱。

2.其次蚜锨,有人會建議你去將特定的view holder定義成不可復(fù)用。

holder.setIsRecyclable(false);

或是

recyclerView.getRecycledViewPool()
            .setMaxRecycledViews(TARGET_VIEW_HOLDER, POOL_CAPACITY);
//即設(shè)置 POOL_CAPACITY 為 0.

它將幫助你實現(xiàn)第二點即對相同的類型且不同位置不去使用view holder慢蜓,不會將它移動到RecyclerViewPool亚再。但是,它不會在所有情況下都滿足我們的目的晨抡,因為氛悬,當(dāng)我們將目標(biāo)view holder從可見區(qū)域移開時,RecyclerView仍然會從cache中清除它耘柱。它只是沒有移到RecyclerViewPool中如捅。

進階方案

RecyclerView提供了一個回調(diào)方法:

ViewCacheExtension()

RecyclerView在檢查了它自身的cache或scrap view之后以及在將其移動到RecyclerViewPool之前會調(diào)用它的方法 getViewForPositionAndType 。這是一個抽象類调煎,你可以實現(xiàn)它并返回特定位置的視圖伪朽。

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

我們可以利用它來緩存特定的視圖類型。

讓我們看看如何使用它來達到我們的目的汛蝙,并創(chuàng)建一個通用的解決方案烈涮,使它允許為特定的視圖類型啟用緩存。

1.選取特定的view holder去緩存

我們需要一個存儲view holder的Map用來保存這些已經(jīng)緩存的視圖窖剑。我們還需要存儲需要啟用緩存的視圖holder類型坚洽,這里我們使用Set。

val cachedItems: MutableMap<Long, ViewHolder> = HashMap()  
//這里的Key是唯一的用來標(biāo)識特定位置的item
val cachedViewHolderTypes: MutableSet<Int> = HashSet()

現(xiàn)在填充已經(jīng)創(chuàng)建的視圖的最佳時機是:

onViewDetachedFromWindow()

這是因為視圖已經(jīng)被創(chuàng)建并且已經(jīng)被綁定了一次西土,在它被回收之前讶舰,我們將它保存到我們的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相關(guān)聯(lián)需了,即RecyclerView的getItemId(int pos)方法跳昼。

可能你也注意到了我們將這個holder置為可被回收,否則相同的holder將被我們重用到其他的位置上肋乍,因為一旦劃出屏幕RecyclerView便將其移動到RecyclerView Pool中鹅颊。

2.在onBindView調(diào)用之前返回已緩存的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
        }
    }

在返回已緩存的視圖之前需要做相關(guān)的檢查:

  1. 通過對集合進行查詢墓造,我們檢查對應(yīng)的view holder類型是否已啟用緩存堪伍。
  2. 是否這個holder的item id 和當(dāng)前指向的位置相同锚烦。這是為了確保在我們的adapter已經(jīng)被新的數(shù)據(jù)更新了但是我們的緩存Map還持有舊的view holder的場景不會發(fā)生。這是重要的帝雇,因為在數(shù)據(jù)集更新后涮俄,新的視圖類型被分配到這個位置上,但是尸闸,我們?nèi)匀粸檫@個位置返回舊的view holder彻亲,這將導(dǎo)致仍顯示舊的視圖。

3.確保在數(shù)據(jù)集變更的同時緩存的視圖也會被更新

為了達到當(dāng)數(shù)據(jù)集有變化吮廉,我們就能更新緩存Map的目的苞尝,我們需要利用AdapterDataObserver 觀察數(shù)據(jù)的變化。這樣當(dāng)有任何通知到來我們會得到回調(diào)茧痕。

我們可以將數(shù)據(jù)集變更調(diào)用主要分為兩組來處理:

  1. 使用notifyItemRangeRemoved()移除的緩存項。
  2. 我們在notifyItemRangeMoved()或notifyDataSetChanged()之后移除的view holder恼除。

為了簡化這個數(shù)據(jù)集變化導(dǎo)致的分組踪旷,我們繼承AdapterDataObserver類來實現(xiàn)一個抽象類。

這里包含兩個回調(diào)方法

  1. onChanged() : 我們觀察任何數(shù)據(jù)集的變化除了數(shù)據(jù)被移除這個操作外豁辉。所以令野,我們只是檢查,如果我們的緩存項是否仍然存在于新的集合中徽级。

如果緩存項不在新的集合中气破,這個觀察器將負責(zé)刪除它們。

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) :這里餐抢,我們觀察數(shù)據(jù)的刪除操作现使。

 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中注冊這些回調(diào)方法

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)
}

到這里我們已經(jīng)準(zhǔn)備好了,我們自己實現(xiàn)的為RecyclerView特定view holder類型進行緩存的邏輯旷痕,只需要在onCreateViewHolder ()加入一行代碼即可碳锈。

enableCacheForViewHolderType(viewType);

現(xiàn)在,在被聲明view holder類型的onBindView方法只會執(zhí)行一次欺抗。

完整代碼:

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()
        }
    }

}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末售碳,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子绞呈,更是在濱河造成了極大的恐慌贸人,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,451評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件佃声,死亡現(xiàn)場離奇詭異艺智,居然都是意外死亡,警方通過查閱死者的電腦和手機圾亏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評論 3 394
  • 文/潘曉璐 我一進店門力惯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來碗誉,“玉大人,你說我怎么就攤上這事父晶∠保” “怎么了?”我有些...
    開封第一講書人閱讀 164,782評論 0 354
  • 文/不壞的土叔 我叫張陵甲喝,是天一觀的道長尝苇。 經(jīng)常有香客問我,道長埠胖,這世上最難降的妖魔是什么糠溜? 我笑而不...
    開封第一講書人閱讀 58,709評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮直撤,結(jié)果婚禮上非竿,老公的妹妹穿的比我還像新娘。我一直安慰自己谋竖,他們只是感情好红柱,可當(dāng)我...
    茶點故事閱讀 67,733評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蓖乘,像睡著了一般锤悄。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上嘉抒,一...
    開封第一講書人閱讀 51,578評論 1 305
  • 那天零聚,我揣著相機與錄音,去河邊找鬼些侍。 笑死隶症,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的岗宣。 我是一名探鬼主播沿腰,決...
    沈念sama閱讀 40,320評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼狈定!你這毒婦竟也來了颂龙?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,241評論 0 276
  • 序言:老撾萬榮一對情侶失蹤纽什,失蹤者是張志新(化名)和其女友劉穎措嵌,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體芦缰,經(jīng)...
    沈念sama閱讀 45,686評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡企巢,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,878評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了让蕾。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片浪规。...
    茶點故事閱讀 39,992評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡或听,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出笋婿,到底是詐尸還是另有隱情汤踏,我是刑警寧澤砚哗,帶...
    沈念sama閱讀 35,715評論 5 346
  • 正文 年R本政府宣布哑舒,位于F島的核電站植榕,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏庇配。R本人自食惡果不足惜斩跌,卻給世界環(huán)境...
    茶點故事閱讀 41,336評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望捞慌。 院中可真熱鬧耀鸦,春花似錦、人聲如沸啸澡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽锻霎。三九已至著角,卻和暖如春揪漩,著一層夾襖步出監(jiān)牢的瞬間旋恼,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評論 1 270
  • 我被黑心中介騙來泰國打工奄容, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留冰更,地道東北人。 一個月前我還...
    沈念sama閱讀 48,173評論 3 370
  • 正文 我出身青樓昂勒,卻偏偏與公主長得像蜀细,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子戈盈,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,947評論 2 355

推薦閱讀更多精彩內(nèi)容