RecyclerView的緩存和優(yōu)化
一:RecyclerView緩存的是啥
我們都知道ListView緩存的是ItemView费彼,而RecyclerView緩存的是RecyclerView.ViewHolder,這個(gè)ViewHolder中持有對(duì)應(yīng)的ItemView的所有信息猴仑,比如position、view、width、flag等从绘。
二:RecyclerView的四級(jí)緩存
一級(jí)緩存:屏幕內(nèi)緩存(mAttachedScrap)
屏幕內(nèi)緩存指在屏幕中顯示的ViewHolder吓坚,為了屏幕內(nèi) item 快速復(fù)用而存在,(RecyclerView/ListView具有兩次 onLayout() 過程撵幽,第二次onLayout() 中直接使用第一次 onLayout() 緩存的 View,而不必再創(chuàng)建)礁击。這些ViewHolder會(huì)緩存在mAttachedScrap盐杂、mChangedScrap中。
mChangedScrap 表示數(shù)據(jù)已經(jīng)改變的ViewHolder列表哆窿,需要重新綁定數(shù)據(jù)(調(diào)用onBindViewHolder),該層緩存目的是為了調(diào)用notifyItemChanged(pos)链烈,notifyItemRangeChanged(pos,count)后該位置信息發(fā)生改變的緩存。
mAttachedScrap 表示未與RecyclerView分離的ViewHolder列表挚躯,該層緩存目的是在調(diào)用notfyXxx時(shí)未改變的item测垛,以及影響RecyclerView重新繪制的情況。
二級(jí)緩存:屏幕外緩存(mCachedViews)
用來緩存移除屏幕之外的 ViewHolder秧均,默認(rèn)情況下緩存容量是2食侮,可以通過 setViewCacheSize 方法來改變緩存的容量大小。如果mCachedViews 的容量已滿目胡,根據(jù)FIFO規(guī)則會(huì)優(yōu)先移除舊ViewHolder锯七,把舊ViewHolder移入到緩存池RecycledViewPool 中。mCachedViews中攜帶了原來的ViewHolder的所有數(shù)據(jù)信息誉己,可以直接拿來復(fù)用眉尸。mCachedViews是根據(jù)position 來匹配相應(yīng)的 ViewHolder 的,這里的 position 指的是 RecyclerView 預(yù)測的巨双、可能進(jìn)入屏幕的 item 的 position噪猾,它是由當(dāng)前屏幕滑動(dòng)方向和可見的 item 位置來共同決定的。例如:屏幕向下滑動(dòng)筑累,那么可能進(jìn)入屏幕的 item 的 position 就是當(dāng)前可見第一個(gè) item 的 position - 1袱蜡;屏幕向上滑動(dòng),那么可能進(jìn)入屏幕的 item 的 position 就是當(dāng)前可見的最后一個(gè) item 的 position + 1慢宗。
舉個(gè)栗子:當(dāng)前屏幕內(nèi)第一個(gè)可見的item的position是1坪蚁,用戶進(jìn)行了一個(gè)下拉操作,那么當(dāng)前預(yù)測的position就相當(dāng)于(1-1=0)镜沽,也就是position=0的那個(gè)item要被拉回到屏幕敏晤,此時(shí)RecyclerView就從cache里面找position=0的數(shù)據(jù),如果找到了就拿來直接復(fù)用缅茉。
三級(jí)緩存:自定義緩存(ViewCacheExtension)
給用戶的自定義擴(kuò)展緩存嘴脾,需要用戶自己管理View 的創(chuàng)建和緩存,可通過RecyclerView.setViewCacheExtension()設(shè)置蔬墩。
四級(jí)緩存:緩存池(RecycledViewPool)
ViewHolder 緩存池译打,在mCachedViews中如果緩存已滿的時(shí)候(默認(rèn)最大值為2個(gè))耗拓,先把mCachedViews中舊的ViewHolder 存入到RecyclerViewPool。如果RecyclerViewPool緩存池已滿扶平,就不會(huì)再緩存帆离。從緩存池中取出的ViewHolder 蔬蕊,需要重新調(diào)用onBindViewHolder綁定數(shù)據(jù)结澄。
按照 ViewType 來查找ViewHolder
每個(gè) ViewType 默認(rèn)最多緩存 5 個(gè)
可以多個(gè) RecyclerView 共享RecycledViewPool
為啥要有第四緩: 可以由開發(fā)者主動(dòng)向內(nèi)填充數(shù)據(jù)(RecycledViewPool#putRecycledView(ViewHolder)),技術(shù)上可以實(shí)現(xiàn)多個(gè) RecyclerView 共用同一個(gè)RecyclerViewPool岸夯。
三:RecyclerView的緩存策略
按四級(jí)緩存的策略查找麻献,沒有找到就創(chuàng)建(如果取到直接丟給rv來展示,如果取不到最終才會(huì)執(zhí)行熟悉的onCreateViewHolder和onBindViewHolder方法)猜扮,其中只有RecyclerViewPool找到時(shí)才會(huì)調(diào)用onBindViewHolder勉吻,流程如下:
四:RecyclerView的緩存過程
場景一:
先看圖的左邊(此時(shí)假設(shè)cache 與 pool 中沒有東西),當(dāng)向下滑動(dòng)時(shí)旅赢,3 最先進(jìn)入 mCachedViews齿桃,隨后是 4 與 5,5 會(huì)將 3 擠出來煮盼,3 就會(huì)跑到 pool 中去了短纵。
再看圖的右邊,繼續(xù)向下滑動(dòng)時(shí)僵控,4 被 6 擠出來香到,放到了 pool 中,同時(shí) 8 需要顯示报破,那么就會(huì)先從 pool 中取悠就,發(fā)現(xiàn)正好有一個(gè) 3,那么就會(huì)取出來充易,將 3 重新顯示到屏幕上梗脾。
場景二:
如果向下滑到 7 顯示出來之后,不再繼續(xù)向下盹靴,而是往上滑動(dòng)藐唠,那么又會(huì)怎么樣呢?
看圖的右邊鹉究,很明顯宇立,5 從 cache 中被取出來直接復(fù)用,不用重新綁定自赔,7 被放入了 cache 中妈嘹。
五:RecyclerView的優(yōu)化
RecyclerView做性能優(yōu)化要說復(fù)雜也復(fù)雜,比如說布局優(yōu)化绍妨、緩存润脸、預(yù)加載等等柬脸。
優(yōu)化的點(diǎn)有很多,在這些看似獨(dú)立的點(diǎn)之間毙驯,其實(shí)存在一個(gè)樞紐:Adapter倒堕。
因?yàn)樗械腣iewHolder的創(chuàng)建和內(nèi)容的綁定都需要經(jīng)過Adaper的兩個(gè)函數(shù)onCreateViewHolder和onBindViewHolder。
因此我們性能優(yōu)化的本質(zhì)就是要**減少這兩個(gè)函數(shù)的調(diào)用時(shí)間和調(diào)用的次數(shù)**爆价。
1.從減少方法的調(diào)用次數(shù)來看:
(1)setItemViewCaches(int)
RecyclerView可以設(shè)置自己所需要的ViewHolder的CacheViews緩存數(shù)量垦巴,默認(rèn)大小是2。CacheViews中的緩存只能position相同才可得用铭段,且不會(huì)重新bindView骤宣,CacheViews滿了后移除到RecyclerPool中,并重置ViewHolder序愚,如果對(duì)于可能來回滑動(dòng)的RecyclerView憔披,把CacheViews的緩存數(shù)量設(shè)置大一些,可以減少bindView的次數(shù)爸吮,加快布局顯示芬膝。
注:此方法是拿空間換時(shí)間,要充分考慮應(yīng)用內(nèi)存問題形娇,根據(jù)應(yīng)用實(shí)際使用情況設(shè)置大小锰霜。
(2).setRecyclerViewPoll(復(fù)用poll緩存)
RecyclerView設(shè)置一個(gè)ViewHolder的對(duì)象池,這個(gè)池稱為RecycledViewPool埂软,這個(gè)對(duì)象池可以節(jié)省創(chuàng)建ViewHolder的開銷锈遥,更能避免GC,即便你不給它設(shè)置勘畔,它也會(huì)自己創(chuàng)建一個(gè)所灸。
如果多個(gè)RecycledView 的 Adapter 是一樣的,比如嵌套的 RecyclerView 中存在一樣的 Adapter炫七,可以通過設(shè)置RecyclerView.setRecycledViewPool(pool)來共用一個(gè)RecycledViewPool爬立。
RecycledViewPool使用:先從某個(gè)RecyclerView對(duì)象中獲得它創(chuàng)建的RecycledViewPool對(duì)象,或者是自己實(shí)現(xiàn)一個(gè)RecycledViewPool對(duì)象万哪,然后設(shè)置個(gè)接下來創(chuàng)建的每一個(gè)RecyclerView即可侠驯。
應(yīng)用場景:
a).針對(duì)item中包含rv的情況下才適用,如果rv的item都是普通的布局就不需要復(fù)用poll
b).Tabs+ViewPager+RecyclerView
c).一個(gè)豎直的RecyclerView包含多行可分別左右滑動(dòng)的RecyclerView
(3).RecyclerView.getRecycledViewPool().setMaxRecycledViews(0, 20)
當(dāng)我們調(diào)用notifyDataSetChanged() 或者notifyItemRangeChanged(i, c) (c這個(gè)范圍非常大的時(shí)候)奕巍,那么很多 ViewHolder 都會(huì)最終被放入到 pool 中吟策,因?yàn)?pool 只能放置5 個(gè),那么多余的就會(huì)被丟棄的止,等待回收檩坚。最重要的是會(huì)重新 create 與 bind 對(duì)性能影響比較大。如果你的列表能夠容納很多行,而且使用notifyDataSetChanged 方法比較頻繁匾委,那么你應(yīng)該考慮設(shè)置一下容量大小拖叙。
(4).使用局部刷新
調(diào)用了notifyDataSetChanged方法,RecyclerView 不知道到底發(fā)生了什么赂乐,所以它只能認(rèn)為所有的東西都發(fā)生了變化薯鳍,即將所有的 ViewHolder 都放入到 pool 中。會(huì)導(dǎo)致整個(gè)頁面范圍內(nèi)的ViewHolder重新調(diào)用onBindViewHolder方法這樣就重復(fù)做了一次bind操作挨措。這時(shí)我們換用notifyItemRemoved方法挖滤。可以看到运嗜,這時(shí)只會(huì)由于第一個(gè)移除壶辜,導(dǎo)致新的一個(gè)position=8進(jìn)入并展示悯舟,所以只有position=8調(diào)用了onBindViewHodler方法担租,而其他的已經(jīng)綁定的ViewHolder不需要重新綁定。
2.從減少方法執(zhí)行的時(shí)間來看:
(1).布局優(yōu)化 降低item的布局層次抵怎,使用ConstraintLayout奋救。
(2).去除冗余的setItemClick事件,一般都是在onBind方法中設(shè)置監(jiān)聽反惕,但是onBindView調(diào)用時(shí)機(jī)很多尝艘,會(huì)導(dǎo)致在RecyclerView滑動(dòng)過程中創(chuàng)建很多對(duì)象,這時(shí)可以全局創(chuàng)建一個(gè)姿染。
(3).避免在onBindView中進(jìn)行耗時(shí)的操作背亥。
其他方面的優(yōu)化:
(1).設(shè)置高度固定
如果item高度固定,可以使用RecyclerView.setHasFixedSize(true)來避免requestLayout浪費(fèi)資源悬赏。
notify一系列方法會(huì)執(zhí)行到下面這個(gè)方法
區(qū)別就在于當(dāng)設(shè)置setHasFixedSize會(huì)走if分支狡汉,而沒有設(shè)置則進(jìn)入到else分支,else分支直接會(huì)調(diào)用到requestLayout方法闽颇,該方法會(huì)導(dǎo)致視圖樹進(jìn)行重新繪制盾戴,onMeasure,onLayout最終都會(huì)被執(zhí)行到兵多,根據(jù)上述源碼可以得到一個(gè)優(yōu)化的地方在于尖啡,當(dāng)item嵌套了rv并且rv沒有設(shè)置wrap_content屬性時(shí),我們可以對(duì)該rv設(shè)置setHasFixedSize剩膘,這么做的一個(gè)最大的好處就是嵌套的rv不會(huì)觸發(fā)requestLayout衅斩,從而不會(huì)導(dǎo)致外層的rv進(jìn)行重繪。
(2).增加RecyclerView預(yù)留的額外空間
額外空間:顯示范圍之外怠褐,應(yīng)該額外緩存的空間
new LinearLayoutManager(this) {
??? @Override
??? protected int getExtraLayoutSpace(RecyclerView.State state) {
??????? return size;
??? }
};
一屏只能顯示一個(gè)元素的時(shí)候畏梆,第一次滑動(dòng)到第二個(gè)元素會(huì)卡頓。
RecyclerView?(以及其他基于adapter的view,比如ListView具温、GridView等)使用了緩存機(jī)制重用子 view(簡而言之就是蚕涤,系統(tǒng)只將屏幕可見范圍之內(nèi)的元素保存在內(nèi)存中,在滾動(dòng)的時(shí)候不斷的重用這些內(nèi)存中已經(jīng)存在的view铣猩,而不是新建view)揖铜。
這個(gè)機(jī)制在我們這里會(huì)導(dǎo)致一個(gè)問題,啟動(dòng)應(yīng)用之后达皿,在屏幕可見范圍內(nèi)天吓,我們只有一張卡片可見(估計(jì)作者的屏幕比較小)峦椰,當(dāng)我們滾動(dòng)的時(shí) 候龄寞,RecyclerView找不到可以重用的view了,它將創(chuàng)建一個(gè)新的汤功,因此在滑動(dòng)到第二個(gè)feed的時(shí)候就會(huì)有一定的延時(shí)物邑,但是第二個(gè)feed之 后的滾動(dòng)是流暢的,因?yàn)檫@個(gè)時(shí)候RecyclerView已經(jīng)有能重用的view了滔金。
getExtraLayoutSpace將返回LayoutManager應(yīng)該預(yù)留的額外空間(顯示范圍之外色解,應(yīng)該額外緩存的空間)。
(3).swapAdapter
我們使用RecyclerView時(shí)候餐茵,一般是setAdapter一次科阎,之后通過調(diào)用adapter.notify()來更新數(shù)據(jù)和UI(不討論差量更新)。一個(gè)界面中由一個(gè)RecyclerView承載所有內(nèi)容忿族,但是可以通過界面內(nèi)tab_button來切換內(nèi)容類別的情況锣笨,用于內(nèi)容數(shù)據(jù)量較大,希望來回切換能流暢迅速道批。因此這里我采用了多個(gè)adapter來記錄不同的類別數(shù)據(jù)错英,來回切換只要調(diào)用setAdapter(Adapter adapter)即可這是一個(gè)和setAdapter類似的方法,不過針對(duì)于界面view結(jié)構(gòu)類似或者相同屹徘,需要頻繁設(shè)置adapter的時(shí)候走趋,做了優(yōu)化,能夠再切換的時(shí)候復(fù)用相同的viewHolder噪伊,減少一定的開銷簿煌。
(4).DiffUtil一個(gè)神奇的工具類
DiffUtil是配合rv進(jìn)行差異化比較的工具類,通過對(duì)比前后兩個(gè)data數(shù)據(jù)集合鉴吹,DiffUtil會(huì)自動(dòng)給出一系列的notify操作姨伟,避免我們手動(dòng)調(diào)用notifiy的繁瑣.
但DiffUtil不太好用,有弊端:
(1).必須準(zhǔn)備兩個(gè)數(shù)據(jù)集
(2).實(shí)現(xiàn)callback接口豆励,areContentsTheSame是最難實(shí)現(xiàn)的夺荒,涉及到對(duì)比同type的item內(nèi)容是否一致瞒渠,比較效率的問題