365,recyclerView的優(yōu)點(diǎn)和復(fù)用機(jī)制

一、RecyclerView是什么

RecycleView是Android5.0后谷歌推出的一個(gè)用于在有限的窗口中展示大量數(shù)據(jù)集的控件邮旷,位于support-v7包中。它可以實(shí)現(xiàn)與ListView和GridView一樣的效果曙求,提供了一種插拔式的體驗(yàn)卖词,高度的解耦,異常的靈活墨榄,只需設(shè)置其提供的不同的LayoutManager玄糟,ItemAnimator和ItemDecoration,就能實(shí)現(xiàn)不同的效果袄秩。

二阵翎、RecyclerView的優(yōu)點(diǎn)

1、支持局部刷新之剧。
2郭卫、可以自定義item增刪時(shí)的動(dòng)畫。
3背稼、能夠?qū)崿F(xiàn)item拖拽和側(cè)滑刪除等功能贰军。
4、默認(rèn)已實(shí)現(xiàn)View的復(fù)用蟹肘,而且回收機(jī)制更加完善词疼。

三、RecyclerView回收復(fù)用機(jī)制淺析

RecyclerView回收復(fù)用機(jī)制淺析

RecyclerView 基本上已經(jīng)成為了開發(fā)中常用的一個(gè)組件帘腹,通過其提供的強(qiáng)大能力贰盗,能實(shí)現(xiàn)各種需要的列表類效果。靈活的同時(shí)阳欲,要用好卻也不容易童太,為了高效實(shí)現(xiàn)需求,避免掉到各種不明所以的坑里面胸完,這里有必要對其回收復(fù)用機(jī)制做一個(gè)探究。

本文將帶著下面這幾個(gè)方面的問題來探究翘贮。tips:結(jié)合源碼食用更佳赊窥。

開始分析回收復(fù)用機(jī)制之前,先提幾個(gè)問題:

Q1:如果向下滑動(dòng)狸页,新一行的5個(gè)卡位的顯示會去復(fù)用緩存的 ViewHolder锨能,第一行的5個(gè)卡位會移出屏幕被回收扯再,那么在這個(gè)過程中,是先進(jìn)行復(fù)用再回收址遇?還是先回收再復(fù)用熄阻?還是邊回收邊復(fù)用?也就是說倔约,新一行的5個(gè)卡位復(fù)用的 ViewHolder 有可能是第一行被回收的5個(gè)卡位嗎?

第二個(gè)問題之前,先看幾張圖片:


image.png

這邊的理解先向下滑動(dòng)唧席,指的是手指向上滑動(dòng)娄涩,一直到List的最底層,這邊向上或者向下绢要,指的是RecyclerView的頂端和底端

黑框表示屏幕吏恭,RecyclerView 先向下滑動(dòng),第三行卡位顯示出來重罪,再向上滑動(dòng)樱哼,第三行移出屏幕,第一行顯示出來剿配。我們分別在 Adapter 的 onCreateViewHolder() 和 onBindViewHolder() 里打日志搅幅,下面是這個(gè)過程的日志:


image.png

紅框1是 RecyclerView 向下滑動(dòng)操作的日志,第三行5個(gè)卡位的顯示都是重新創(chuàng)建的 ViewHolder 惨篱;紅框2是再次向上滑動(dòng)時(shí)的日志盏筐,第一行5個(gè)卡位的重新顯示用的 ViewHolder 都是復(fù)用的,因?yàn)闆]有 create viewHolder 的日志砸讳,然后只有后面3個(gè)卡位重新綁定數(shù)據(jù)琢融,調(diào)用了onBindViewHolder();那么問題來了:

Q2: 在這個(gè)過程中簿寂,為什么當(dāng) RecyclerView 再次向上滑動(dòng)重新顯示第一行的5個(gè)卡位時(shí)漾抬,只有后面3個(gè)卡位觸發(fā)了 onBindViewHolder() 方法,重新綁定數(shù)據(jù)呢常遂?明明5個(gè)卡位都是復(fù)用的纳令。

在上面的操作基礎(chǔ)上,我們繼續(xù)往下操作:

image.png

在第二個(gè)問題操作的基礎(chǔ)上克胳,目前已經(jīng)創(chuàng)建了15個(gè) ViewHolder平绩,此時(shí)顯示的是第1、2行的卡位漠另,那么繼續(xù)向下滑動(dòng)兩次捏雌,這個(gè)過程的日志如下:

image.png

紅框1是第二個(gè)問題操作的日志,在這里截出來只是為了顯示接下去的日志是在上面的基礎(chǔ)上繼續(xù)操作的笆搓;

紅框2就是第一次向下滑時(shí)的日志性湿,對比問題2的日志纬傲,這次第三行的5個(gè)卡位用的 ViewHolder 也都是復(fù)用的,而且也只有后面3個(gè)卡位觸發(fā)了 onBindViewHolder() 重新綁定數(shù)據(jù)肤频;

紅框3是第二次向下滑動(dòng)時(shí)的日志叹括,這次第四行的5個(gè)卡位,前3個(gè)的卡位用的 ViewHolder 是復(fù)用的宵荒,后面2個(gè)卡位的 ViewHolder 則是重新創(chuàng)建的汁雷,而且5個(gè)卡位都調(diào)用了 onBindViewHolder() 重新綁定數(shù)據(jù);

那么骇扇,

Q3:接下去不管是向上滑動(dòng)還是向下滑動(dòng)摔竿,滑動(dòng)幾次,都不會再有 onCreateViewHolder() 的日志了少孝,也就是說 RecyclerView 總共創(chuàng)建了17個(gè) ViewHolder继低,但有時(shí)一行的5個(gè)卡位只有3個(gè)卡位需要重新綁定數(shù)據(jù),有時(shí)卻又5個(gè)卡位都需要重新綁定數(shù)據(jù)稍走,這是為什么呢袁翁?

如果明白 RecyclerView 的回收復(fù)用機(jī)制,那么這三個(gè)問題也就都知道原因了婿脸;反過來粱胜,如果知道這三個(gè)問題的原因,那么理解 RecyclerView 的回收復(fù)用機(jī)制也就更簡單了狐树;所以焙压,帶著問題,在特定的場景下去分析源碼的話抑钟,應(yīng)該會比較容易涯曲。

源碼分析

其實(shí),根據(jù)問題2的日志在塔,我們就可以回答問題1了幻件。在目前顯示1、2行蛔溃,
ViewHolder 的個(gè)數(shù)為10個(gè)的基礎(chǔ)上绰沥,第三行的5個(gè)新卡位要顯示出來都需要重新創(chuàng)建 ViewHolder,也就是說贺待,在這個(gè)向下滑動(dòng)的過程徽曲,是5個(gè)新卡位的復(fù)用機(jī)制先進(jìn)行工作,然后第1行的5個(gè)被移出屏幕的卡位再進(jìn)行回收機(jī)制工作麸塞。

那么疟位,就先來看看復(fù)用機(jī)制的源碼

復(fù)用機(jī)制
image.png

image.png

image.png

這個(gè)方法是復(fù)用機(jī)制的入口,也就是 Recycler 開放給外部使用復(fù)用機(jī)制的api喘垂,外部調(diào)用這個(gè)方法就可以返回想要的 View甜刻,而至于這個(gè) View 是復(fù)用而來的,還是重新創(chuàng)建得來的正勒,就都由 Recycler 內(nèi)部實(shí)現(xiàn)得院,對外隱藏。

tryGetViewHolderForPositionByDeadline()

所以章贞,Recycler 的復(fù)用機(jī)制內(nèi)部實(shí)現(xiàn)就在這個(gè)方法里祥绞。
分析邏輯之前,先看一下 Recycler 的幾個(gè)結(jié)構(gòu)體鸭限,用來緩存 ViewHolder 的蜕径。

image.png

mAttachedScrap: 用于緩存顯示在屏幕上的 item 的 ViewHolder,場景好像是 RecyclerView 在 onLayout 時(shí)會先把 children 都移除掉败京,再重新添加進(jìn)去兜喻,所以這個(gè) List 應(yīng)該是用在布局過程中臨時(shí)存放 children 的,反正在 RecyclerView 滑動(dòng)過程中不會在這里面來找復(fù)用的 ViewHolder 就是了赡麦。

mChangedScrap: 這個(gè)沒理解是干嘛用的朴皆,看名字應(yīng)該跟 ViewHolder 的數(shù)據(jù)發(fā)生變化時(shí)有關(guān)吧,在 RecyclerView 滑動(dòng)的過程中泛粹,也沒有發(fā)現(xiàn)到這里找復(fù)用的 ViewHolder遂铡,所以這個(gè)可以先暫時(shí)放一邊。

mCachedViews:這個(gè)就重要得多了晶姊,滑動(dòng)過程中的回收和復(fù)用都是先處理的這個(gè) List扒接,這個(gè)集合里存的 ViewHolder 的原本數(shù)據(jù)信息都在,所以可以直接添加到 RecyclerView 中顯示们衙,不需要再次重新 onBindViewHolder()钾怔。

mUnmodifiableAttachedScrap: 不清楚干嘛用的,暫時(shí)跳過砍艾。

mRecyclerPool:這個(gè)也很重要蒂教,但存在這里的 ViewHolder 的數(shù)據(jù)信息會被重置掉,相當(dāng)于 ViewHolder 是一個(gè)重創(chuàng)新建的一樣脆荷,所以需要重新調(diào)用 onBindViewHolder 來綁定數(shù)據(jù)凝垛。

mViewCacheExtension:這個(gè)是留給我們自己擴(kuò)展的,好像也沒怎么用蜓谋,就暫時(shí)不分析了梦皮。

image.png

第一步很簡單,position 如果在 item 的范圍之外的話桃焕,那就拋異常吧剑肯。繼續(xù)往下看

image.png

如果是在 isPreLayout() 時(shí),那么就去 mChangedScrap 中找观堂。
那么這個(gè) isPreLayout 表示的是什么让网?呀忧,有兩個(gè)賦值的地方。

image.png

emmm溃睹,看樣子而账,在 LayoutManager 的 onLayoutChildren 前就會置為
false,不過我還是不懂這個(gè)過程是干嘛的因篇,滑動(dòng)過程中好像
mState.mInPreLayou = false泞辐,所以并不會來這里,先暫時(shí)跳過竞滓。繼續(xù)往下咐吼。

image.png

跟進(jìn)這個(gè)方法看看

image.png

首先,去 mAttachedScrap 中尋找 position 一致的 viewHolder商佑,需要匹配一些條件锯茄,大致是這個(gè) viewHolder 沒有被移除,是有效的之類的條件莉御,滿足就返回這個(gè) viewHolder撇吞。

所以,這里的關(guān)鍵就是要理解這個(gè) mAttachedScrap 到底是什么礁叔,存的是哪些 ViewHolder牍颈。

一次遙控器按鍵的操作,不管有沒有發(fā)生滑動(dòng)琅关,都會導(dǎo)致 RecyclerView 的重新 onLayout煮岁,那要 layout 的話,RecyclerView 會先把所有 children 先 remove 掉涣易,然后再重新 add 上去画机,完成一次 layout 的過程。那么這暫時(shí)性的 remove 掉的 viewHolder 要存放在哪呢新症,就是放在這個(gè) mAttachedScrap 中了步氏,這就是我的理解了。

所以徒爹,感覺這個(gè) mAttachedScrap 中存放的 viewHolder 跟回收和復(fù)用關(guān)系不大荚醒。

網(wǎng)上一些分析的文章有說,RecyclerView 在復(fù)用時(shí)會按順序去 mChangedScrap, mAttachedScrap 等等緩存里找隆嗅,沒有找到再往下去找界阁,從代碼上來看是這樣沒錯(cuò),但我覺得這樣表述有問題胖喳。因?yàn)榫臀覀冞@篇文章基于 RecyclerView 的滑動(dòng)場景來說泡躯,新卡位的復(fù)用以及舊卡位的回收機(jī)制,其實(shí)都不會涉及到mChangedScrap 和 mAttachedScrap,所以我覺得還是基于某種場景來分析相對應(yīng)的回收復(fù)用機(jī)制會比較好较剃。就像mChangedScrap 我雖然沒理解是干嘛用的咕别,但我猜測應(yīng)該是在當(dāng)數(shù)據(jù)發(fā)生變化時(shí)才會涉及到的復(fù)用場景,所以當(dāng)我分析基于滑動(dòng)場景時(shí)的復(fù)用時(shí)写穴,即使我對這塊不理解顷级,影響也不會很大。

繼續(xù)往下看

image.png

emmm确垫,這段也還是沒看懂,但估計(jì)應(yīng)該需要一些特定的場景下所使用的復(fù)用策略吧帽芽,看名字删掀,應(yīng)該跟 hidden 有關(guān)?不懂导街,跳過這段披泪,應(yīng)該也沒事,滑動(dòng)過程中的回收復(fù)用跟這個(gè)應(yīng)該也關(guān)系不大搬瑰。

image.png

這里就要畫重點(diǎn)啦款票,記筆記記筆記,滑動(dòng)場景中的復(fù)用會用到這里的機(jī)制泽论。

mCachedViews 的大小默認(rèn)為2艾少。遍歷 mCachedViews,找到 position 一致的 ViewHolder翼悴,之前說過缚够,mCachedViews 里存放的 ViewHolder 的數(shù)據(jù)信息都保存著,所以 mCachedViews 可以理解成鹦赎,只有原來的卡位可以重新復(fù)用這個(gè) ViewHolder谍椅,新位置的卡位無法從 mCachedViews 里拿 ViewHolder出來用。

找到 viewholder 后


image.png

就算 position 匹配找到了 ViewHolder古话,還需要判斷一下這個(gè) ViewHolder 是否已經(jīng)被 remove 掉雏吭,type 類型一致不一致,如下陪踩。

image.png

以上是在 mCachedViews 中尋找杖们,沒有找到的話,就繼續(xù)再找一遍膊毁,剛才是通過 position 來找胀莹,那這次就換成id,然后重復(fù)上面的步驟再找一遍婚温,如下

image.png

getScrapOrCachedViewForId() 做的事跟 getScrapOrHiddenOrCacheHolderForPosition() 其實(shí)差不多描焰,只不過一個(gè)是通過 position 來找 ViewHolder,一個(gè)是通過 id 來找。而這個(gè) id 并不是我們在 xml 中設(shè)置的 android:id荆秦, 而是 Adapter 持有的一個(gè)屬性篱竭,默認(rèn)是不會使用這個(gè)屬性的,所以這個(gè)第5步其實(shí)是不會執(zhí)行的步绸,除非我們重寫了 Adapter 的 setHasStableIds()掺逼,既然不是常用的場景,那就先略過吧瓤介,那就繼續(xù)往下吕喘。

image.png

這個(gè)就是常說擴(kuò)展類了,RecyclerView 提供給我們自定義實(shí)現(xiàn)的擴(kuò)展類刑桑,我們可以重寫 getViewForPositionAndType() 方法來實(shí)現(xiàn)自己的復(fù)用策略氯质。不過,也沒用過祠斧,那這部分也當(dāng)作不會執(zhí)行闻察,略過。繼續(xù)往下

image.png

這里也是重點(diǎn)了琢锋,記筆記記筆記辕漂。

這里是去 RecyclerViewPool 里取 ViewHolder,ViewPool 會根據(jù)不同的 item type 創(chuàng)建不同的 List吴超,每個(gè) List 默認(rèn)大小為5個(gè)钉嘹。看一下去 ViewPool 里是怎么找的

image.png

之前說過烛芬,ViewPool 會根據(jù)不同的 viewType 創(chuàng)建不同的集合來存放 ViewHolder隧期,那么復(fù)用的時(shí)候,只要 ViewPool 里相同的 type 有 ViewHolder 緩存的話赘娄,就將最后一個(gè)拿出來復(fù)用仆潮,不用像 mCachedViews 需要各種匹配條件,只要有就可以復(fù)用遣臼。

繼續(xù)看"圖第7步"后面的代碼性置,拿到 ViewHolder 之后,還會再次調(diào)用 resetInternal() 來重置 ViewHolder揍堰,這樣 ViewHolder 就可以當(dāng)作一個(gè)全新的 ViewHolder 來使用了鹏浅,這也就是為什么從這里拿的 ViewHolder 都需要重新 onBindViewHolder() 了

那如果在 ViewPool 里還是沒有找到呢屏歹,繼續(xù)往下看

image.png

如果 ViewPool 中都沒有找到 ViewHolder 來使用的話隐砸,那就調(diào)用 Adapter 的 onCreateViewHolder 來創(chuàng)建一個(gè)新的 ViewHolder 使用。

上面一共有很多步驟來找 ViewHolder蝙眶,不管在哪個(gè)步驟季希,只要找到 ViewHolder 的話褪那,那下面那些步驟就不用管了,然后都要繼續(xù)往下判斷是否需要重新綁定數(shù)據(jù)式塌,還有檢查布局參數(shù)是否合法博敬。如下:

image.png

到這里,tryGetViewHolderForPositionByDeadline() 這個(gè)方法就結(jié)束了峰尝。這大概就是 RecyclerView 的復(fù)用機(jī)制偏窝,中間我們跳過很多地方,因?yàn)?RecyclerView 有各種場景可以刷新他的 view武学,比如重新 setLayoutManager()祭往,重新 setAdapter(),或者 notifyDataSetChanged()火窒,或者滑動(dòng)等等之類的場景链沼,只要重新layout,就會去回收和復(fù)用 ViewHolder沛鸵,所以這個(gè)復(fù)用機(jī)制需要考慮到各種各樣的場景。

把代碼一行行的啃透有點(diǎn)吃力缆八,所以我就只借助 RecyclerView 的滑動(dòng)的這種場景來分析它涉及到的回收和復(fù)用機(jī)制曲掰。

下面就分析一下回收機(jī)制

回收機(jī)制

回收機(jī)制的入口就有很多了,因?yàn)?Recycler 有各種結(jié)構(gòu)體奈辰,比如mAttachedScrap栏妖,mCachedViews 等等,不同結(jié)構(gòu)體回收的時(shí)機(jī)都不一樣奖恰,入口也就多了吊趾。

所以,還是基于 RecyclerView 的滑動(dòng)場景下瑟啃,移出屏幕的卡位回收時(shí)的入口是:

image.png

本篇分析的滑動(dòng)場景论泛,在 RecyclerView 滑動(dòng)時(shí),會交由 LinearLayoutManager 的 scrollVerticallyBy() 去處理蛹屿,然后 LayoutManager 會接著調(diào)用 fill() 方法去處理需要復(fù)用和回收的卡位屁奏,最終會調(diào)用上述 recyclerView() 這個(gè)方法開始進(jìn)行回收工作。

image.png

本篇分析的滑動(dòng)場景错负,在 RecyclerView 滑動(dòng)時(shí)坟瓢,會交由 LinearLayoutManager 的 scrollVerticallyBy() 去處理,然后 LayoutManager 會接著調(diào)用 fill() 方法去處理需要復(fù)用和回收的卡位犹撒,最終會調(diào)用上述 recyclerView() 這個(gè)方法開始進(jìn)行回收工作折联。

image.png
image.png
image.png
image.png
image.png

回收的邏輯比較簡單,由 LayoutManager 來遍歷移出屏幕的卡位识颊,然后對每個(gè)卡位進(jìn)行回收操作诚镰,回收時(shí),都是把 ViewHolder 放在 mCachedViews 里面,如果 mCachedViews 滿了怕享,那就在 mCachedViews 里拿一個(gè) ViewHolder 扔到 ViewPool 緩存里执赡,然后 mCachedViews 就可以空出位置來放新回收的 ViewHolder 了。

總結(jié)一下:

RecyclerView 滑動(dòng)場景下的回收復(fù)用涉及到的結(jié)構(gòu)體兩個(gè):
mCachedViewsRecyclerViewPool
mCachedViews 優(yōu)先級高于 RecyclerViewPool函筋,回收時(shí)沙合,最新的 ViewHolder 都是往 mCachedViews 里放,如果它滿了跌帐,那就移出一個(gè)扔到 ViewPool 里好空出位置來緩存最新的 ViewHolder首懈。

復(fù)用時(shí),也是先到 mCachedViews 里找 ViewHolder谨敛,但需要各種匹配條件究履,概括一下就是只有原來位置的卡位可以復(fù)用存在 mCachedViews 里的 ViewHolder,如果 mCachedViews 里沒有脸狸,那么才去 ViewPool 里找最仑。

在 ViewPool 里的 ViewHolder 都是跟全新的 ViewHolder 一樣,只要 type 一樣炊甲,有找到泥彤,就可以拿出來復(fù)用,重新綁定下數(shù)據(jù)即可卿啡。

整體的流程圖如下:(可放大查看)


image.png

最后吟吝,解釋一下開頭的問題

Q1:如果向下滑動(dòng),新一行的5個(gè)卡位的顯示會去復(fù)用緩存的 ViewHolder颈娜,第一行的5個(gè)卡位會移出屏幕被回收剑逃,那么在這個(gè)過程中,是先進(jìn)行復(fù)用再回收官辽?還是先回收再復(fù)用蛹磺?還是邊回收邊復(fù)用?也就是說同仆,新一行的5個(gè)卡位復(fù)用的 ViewHolder 有可能是第一行被回收的5個(gè)卡位嗎称开?

答:先復(fù)用再回收,新一行的5個(gè)卡位先去目前的 mCachedViews 和 ViewPool 的緩存中尋找復(fù)用乓梨,沒有就重新創(chuàng)建鳖轰,然后移出屏幕的那行的5個(gè)卡位再回收緩存到 mCachedViews 和 ViewPool 里面,所以新一行5個(gè)卡位和復(fù)用不可能會用到剛移出屏幕的5個(gè)卡位扶镀。

Q2: 在這個(gè)過程中蕴侣,為什么當(dāng) RecyclerView 再次向上滑動(dòng)重新顯示第一行的5個(gè)卡位時(shí),只有后面3個(gè)卡位觸發(fā)了 onBindViewHolder() 方法臭觉,重新綁定數(shù)據(jù)呢昆雀?明明5個(gè)卡位都是復(fù)用的辱志。

答:滑動(dòng)場景下涉及到的回收和復(fù)用的結(jié)構(gòu)體是 mCachedViews 和 ViewPool,前者默認(rèn)大小為2狞膘,后者為5揩懒。所以,當(dāng)?shù)谌酗@示出來后挽封,第一行的5個(gè)卡位被回收已球,回收時(shí)先緩存在 mCachedViews,滿了再移出舊的到 ViewPool 里辅愿,所有5個(gè)卡位有2個(gè)緩存在 mCachedViews 里智亮,3個(gè)緩存在 ViewPool,至于是哪2個(gè)緩存在 mCachedViews点待,這是由 LayoutManager 控制阔蛉。

上面講解的例子使用的是 GridLayoutManager,滑動(dòng)時(shí)的回收邏輯則是在父類 LinearLayoutManager 里實(shí)現(xiàn)癞埠,回收第一行卡位時(shí)是從后往前回收状原,所以最新的兩個(gè)卡位是0、1苗踪,會放在 mCachedViews 里遭笋,而2、3徒探、4的卡位則放在 ViewPool 里。

所以喂窟,當(dāng)再次向上滑動(dòng)時(shí)测暗,第一行5個(gè)卡位會去兩個(gè)結(jié)構(gòu)體里找復(fù)用,之前說過磨澡,mCachedViews 里存放的 ViewHolder 只有原本位置的卡位才能復(fù)用碗啄,所以0、1兩個(gè)卡位都可以直接去 mCachedViews 里拿 ViewHolder 復(fù)用稳摄,而且這里的 ViewHolder 是不用重新綁定數(shù)據(jù)的稚字,至于2、3厦酬、4卡位則去 ViewPool 里找胆描,剛好 ViewPool 里緩存著3個(gè) ViewHolder,所以第一行的5個(gè)卡位都是用的復(fù)用的仗阅,而從 ViewPool 里拿的復(fù)用需要重新綁定數(shù)據(jù)昌讲,才會這樣只有三個(gè)卡位需要重新綁定數(shù)據(jù)。

Q3:接下去不管是向上滑動(dòng)還是向下滑動(dòng)减噪,滑動(dòng)幾次短绸,都不會再有 onCreateViewHolder() 的日志了车吹,也就是說 RecyclerView 總共創(chuàng)建了17個(gè) ViewHolder,但有時(shí)一行的5個(gè)卡位只有3個(gè)卡位需要重新綁定數(shù)據(jù)醋闭,有時(shí)卻又5個(gè)卡位都需要重新綁定數(shù)據(jù)窄驹,這是為什么呢?

答:有時(shí)一行只有3個(gè)卡位需要重新綁定的原因跟Q2一樣证逻,因?yàn)?mCachedView 里正好緩存著當(dāng)前位置的 ViewHolder乐埠,本來就是它的 ViewHolder 當(dāng)然可以直接拿來用。而至于為什么會創(chuàng)建了17個(gè) ViewHolder瑟曲,那是因?yàn)樵俚谒男械目ㄎ灰@示出來時(shí)饮戳,ViewPool 里只有3個(gè)緩存,而第四行的卡位又用不了 mCachedViews 里的2個(gè)緩存洞拨,因?yàn)檫@兩個(gè)緩存的是6扯罐、7卡位的 ViewHolder,所以就需要再重新創(chuàng)建2個(gè) ViewHodler 來給第四行最后的兩個(gè)卡位使用烦衣。

RecyclerView回收復(fù)用機(jī)制總結(jié)

復(fù)用命中流程圖

image.png

RecyclerView中每層緩存有什么作用歹河?

mChangedScrap

該層緩存目的是為了當(dāng)調(diào)用notifyItemChanged(pos),notifyItemRangeChanged(pos,count)后該位置信息發(fā)生改變的緩存,一般用于change動(dòng)畫,注意mChangedScrap并不是說存儲改變的位置并直接復(fù)用花吟,而是在預(yù)布局時(shí)存儲改變的holder秸歧,重新創(chuàng)建新holder并綁定數(shù)據(jù)來充當(dāng)改變位置的數(shù)據(jù)刷新,然后根據(jù)新老holder執(zhí)行change動(dòng)畫。動(dòng)畫執(zhí)行完畢后新的holder會被緩存到mRecyclerPool中衅澈。那如何復(fù)用notifyItemChanged(pos)改變的holder呢键菱?答案在 mAttachedScrap vs mChangedScrap

mAttachedScrap

該層緩存目的是在調(diào)用notfyXxx時(shí)未改變的item,以及影響RecyclerView重新繪制的情況今布。

mChangedScrap和mAttachedScrap可以看做是一個(gè)層級经备,都是屏幕上可見itemView,只不過區(qū)分了狀態(tài)(改變和未改變)。

mAttachedScrap vs mChangedScrap

Recycler 類中部默,我們可以看到兩個(gè)單獨(dú)的 scrap 容器: mAttachedScrap 和 mChangedScrap侵蒙。為什么需要兩個(gè)呢?
ViewHolder 只有在滿足下面情況才會被添加到 mChangedScrap:當(dāng)它關(guān)聯(lián)的 item 發(fā)生了變化(notifyItemChanged 或者 notifyItemRangeChanged 被調(diào)用)傅蹂,并且 ItemAnimator 調(diào)用 ViewHolder#canReuseUpdatedViewHolder 方法時(shí)纷闺,返回了 false。否則份蝴,ViewHolder 會被添加到mAttachedScrap 中犁功。
canReuseUpdatedViewHolder 返回 “false” 表示我們要執(zhí)行用一個(gè) view 替換另一個(gè) view 的動(dòng)畫,例如淡入淡出動(dòng)畫婚夫。 “true”表示動(dòng)畫在 view 內(nèi)部發(fā)生波桩。
mAttachedScrap 在 整個(gè)布局過程中都能使用,但是mChangedScrap 只能在預(yù)布局階段使用请敦。
這是有道理的:在布局后镐躲,新的 ViewHolder 應(yīng)該替換掉“改變了的”視圖储玫,因此 AttachedScrap 在布局后是沒有用的。 更改動(dòng)畫執(zhí)行完成后萤皂,mChangedScrap 將按預(yù)期方式轉(zhuǎn)存到 mRecyclerPool 中
可以在 3 種情況下重用更新的 ViewHolder:

setSupportsChangeAnimations(false)撒穷。
notifyDataSetChanged 而不是 notifyItemChanged 或 notifyItemRangeChanged
notifyItemChanged(index,anyObject)裆熙。

最后一種情況顯示了一種很好的方法端礼,當(dāng)只想更改一些內(nèi)部元素時(shí),可以避免創(chuàng)建/綁定新的 ViewHolder入录。

mViewCacheExtension

用戶自定義緩存蛤奥,感覺沒什么用。

mCachedViews

作用在滑動(dòng)僚稿,當(dāng)滑進(jìn)屏幕或滑出屏幕凡桥,為了避免多次bind,是一個(gè)大小為2的List

mRecyclerPool

作用在滑動(dòng),當(dāng)超過mCachedViews緩存的大小時(shí)會將mCachedViews最老的數(shù)據(jù)移除放入到mRecyclerPool中
根據(jù)itemType拿 holder集合蚀同,該集合默認(rèn)大小為5缅刽,每次從mRecyclerPool取出的holder都要重置視圖信息,也就是需要從新bind蠢络。當(dāng)mRecyclerPool 找不到緩存的holder時(shí)會調(diào)用adapter的onCreateViewHolder和onBindViewHolder

預(yù)測動(dòng)畫

為什么要調(diào)用notifyXxx后要執(zhí)行兩次布局呢衰猛?一次預(yù)布局,一次實(shí)際布局刹孔?
因?yàn)镽ecyclerView 要執(zhí)行預(yù)測動(dòng)畫啡省。比如有A,B,C三個(gè)itemView,其中A和B被加載到屏幕上髓霞,這時(shí)候刪除B后卦睹,按照最終效果我們會看到C移動(dòng)到B的位置;因?yàn)槲覀冎恢?C 最終的位置酸茴,但是不知道 C 的起始位置在哪里(即C還未被加載)。
第一次 預(yù)先布局
將之前原狀態(tài) 下的 item 都布局出來兢交。并且根據(jù) Adapter 的 notify 信息薪捍,我們知道哪些 item 即將變化了,所以可以加載出另外的 View配喳。在上述例子中酪穿,因?yàn)橹?B 已經(jīng)被刪除了,所以可以把屏幕之外的 C 也加載出來晴裹。
第二次被济,實(shí)際布局,也就是變化完成之后的布局涧团。
這樣只要比較前后布局的變化只磷,就能得出應(yīng)該執(zhí)行什么動(dòng)畫了经磅,就稱為預(yù)測動(dòng)畫。

刷新回收復(fù)用機(jī)制

前面我們知道了調(diào)用notifyXxx后會RecyclerView會進(jìn)行兩次布局钮追,一次預(yù)布局预厌,一次實(shí)際布局,然后執(zhí)行動(dòng)畫操作元媚。具體執(zhí)行方法如下:

  • dispatchLayoutStep1 預(yù)布局
  • dispatchLayoutStep2 實(shí)際布局
  • dispatchLayoutStep3 觸發(fā)動(dòng)畫
  1. dispatchLayoutStep1
    在預(yù)布局時(shí)就會查找改變holder轧叽,并保存在mChangedScrap中;其他未改變的保存到mAttachedScrap中刊棕;在預(yù)布局中獲取holder時(shí)有以下代碼片段炭晒,所以mChangedScrap保存的holder信息只有預(yù)布局時(shí)才會被復(fù)用,另外預(yù)布局后會將舊的holder信息保存甥角,用于在dispatchLayoutStep3中執(zhí)行change動(dòng)畫
//tryGetViewHolderForPositionByDeadline
if (mState.isPreLayout()) {
    holder = getChangedScrapViewForPosition(position);
    fromScrapOrHiddenOrCache = holder != null;
}

其他未改變的位置holder從mAttachedScrap中獲取

  1. dispatchLayoutStep2

實(shí)際布局网严,此步驟會創(chuàng)建一個(gè)新的holder并執(zhí)行綁定數(shù)據(jù),充當(dāng)改變位置的holder蜈膨。因?yàn)閳?zhí)行這一步驟時(shí)mState.isPreLayout就為false屿笼。然后holder為空,就會重新創(chuàng)建holder,并綁定數(shù)據(jù)翁巍。
其他位置holder從mAttachedScrap中獲取

3.dispatchLayoutStep3
獲取新老holder,執(zhí)行change動(dòng)畫驴一,動(dòng)畫完后新的holder會被保存到mRecyclerPool中。
而notifyDataSetChanged會導(dǎo)致RecyclerView上所有可見itemView全部remove,然后緩存在mRecyclerPool中灶壶,而此時(shí)mRecyclerPool中默認(rèn)情況下每種itemType最大緩存5個(gè)肝断,所以當(dāng)緩存滿時(shí),就不會被緩存驰凛。導(dǎo)致接下來獲取緩存holder時(shí)前5個(gè)直接獲取胸懈,但是需要bind,超過5個(gè),會從新adapter的onCreateViewHolder和onBindViewHolder恰响。
所以這也就解釋了為什么notifyItemChanged(pos),notifyItemRangeChanged(pos,count) 會比notifyDataSetChanged高效

滑動(dòng)回收復(fù)用機(jī)制

將滑出屏幕的緩存在mCachedViews中趣钱,默認(rèn)大小為2,如果mCachedViews滿胚宦,則刪除mCachedViews最先被緩存的holder,放入到mRecyclerPool中首有。為什么要先放入到mCachedViews而不是直接放入mRecyclerPool,為什么要這樣做枢劝?
因?yàn)閯偦銎聊坏膇temView可能會被滑動(dòng)進(jìn)來井联,所以加了一層mCachedViews緩存,而從mCachedViews中獲取的holder是不需要重新bind數(shù)據(jù)的您旁。mRecyclerPool取出的holder會被重置信息烙常,重新bind數(shù)據(jù)的。

總結(jié)

mChangedScrap鹤盒,mAttachedScrap 針對的是屏幕可見itemView信息發(fā)生變化時(shí)的回收與復(fù)用
mCachedViews蚕脏,mRecyclerPool 針對的是滑動(dòng)回收與復(fù)用
另外可以通過setItemViewCacheSize 設(shè)置mCachedViews緩存大小侦副,可以通過 recycledViewPool.setMaxRecycledViews() 修改mRecyclerPool緩存大小

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市蝗锥,隨后出現(xiàn)的幾起案子跃洛,更是在濱河造成了極大的恐慌,老刑警劉巖终议,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件汇竭,死亡現(xiàn)場離奇詭異,居然都是意外死亡穴张,警方通過查閱死者的電腦和手機(jī)细燎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來皂甘,“玉大人玻驻,你說我怎么就攤上這事〕フ恚” “怎么了璧瞬?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長渐夸。 經(jīng)常有香客問我婆咸,道長邮屁,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮鹉动,結(jié)果婚禮上借卧,老公的妹妹穿的比我還像新娘职员。我一直安慰自己图张,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布韩肝。 她就那樣靜靜地躺著触菜,像睡著了一般。 火紅的嫁衣襯著肌膚如雪哀峻。 梳的紋絲不亂的頭發(fā)上涡相,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天,我揣著相機(jī)與錄音谜诫,去河邊找鬼漾峡。 笑死攻旦,一個(gè)胖子當(dāng)著我的面吹牛喻旷,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播牢屋,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼且预,長吁一口氣:“原來是場噩夢啊……” “哼槽袄!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起锋谐,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤遍尺,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后涮拗,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體乾戏,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年三热,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了鼓择。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡就漾,死狀恐怖呐能,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情抑堡,我是刑警寧澤摆出,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站首妖,受9級特大地震影響偎漫,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜悯搔,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一骑丸、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧妒貌,春花似錦通危、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至在刺,卻和暖如春逆害,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蚣驼。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工魄幕, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人颖杏。 一個(gè)月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓纯陨,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子翼抠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評論 2 345

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