一、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è)問題之前,先看幾張圖片:
這邊的理解先向下滑動(dòng)唧席,指的是手指向上滑動(dòng)娄涩,一直到List的最底層,這邊向上或者向下绢要,指的是RecyclerView的頂端和底端
黑框表示屏幕吏恭,RecyclerView 先向下滑動(dòng),第三行卡位顯示出來重罪,再向上滑動(dòng)樱哼,第三行移出屏幕,第一行顯示出來剿配。我們分別在 Adapter 的 onCreateViewHolder() 和 onBindViewHolder() 里打日志搅幅,下面是這個(gè)過程的日志:
紅框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ù)往下操作:
在第二個(gè)問題操作的基礎(chǔ)上克胳,目前已經(jīng)創(chuàng)建了15個(gè) ViewHolder平绩,此時(shí)顯示的是第1、2行的卡位漠另,那么繼續(xù)向下滑動(dòng)兩次捏雌,這個(gè)過程的日志如下:
紅框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ī)制
這個(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 的蜕径。
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í)不分析了梦皮。
第一步很簡單,position 如果在 item 的范圍之外的話桃焕,那就拋異常吧剑肯。繼續(xù)往下看
如果是在 isPreLayout() 時(shí),那么就去 mChangedScrap 中找观堂。
那么這個(gè) isPreLayout 表示的是什么让网?呀忧,有兩個(gè)賦值的地方。
emmm溃睹,看樣子而账,在 LayoutManager 的 onLayoutChildren 前就會置為
false,不過我還是不懂這個(gè)過程是干嘛的因篇,滑動(dòng)過程中好像
mState.mInPreLayou = false泞辐,所以并不會來這里,先暫時(shí)跳過竞滓。繼續(xù)往下咐吼。
跟進(jìn)這個(gè)方法看看
首先,去 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ù)往下看
emmm确垫,這段也還是沒看懂,但估計(jì)應(yīng)該需要一些特定的場景下所使用的復(fù)用策略吧帽芽,看名字删掀,應(yīng)該跟 hidden 有關(guān)?不懂导街,跳過這段披泪,應(yīng)該也沒事,滑動(dòng)過程中的回收復(fù)用跟這個(gè)應(yīng)該也關(guān)系不大搬瑰。
這里就要畫重點(diǎn)啦款票,記筆記記筆記,滑動(dòng)場景中的復(fù)用會用到這里的機(jī)制泽论。
mCachedViews 的大小默認(rèn)為2艾少。遍歷 mCachedViews,找到 position 一致的 ViewHolder翼悴,之前說過缚够,mCachedViews 里存放的 ViewHolder 的數(shù)據(jù)信息都保存著,所以 mCachedViews 可以理解成鹦赎,只有原來的卡位可以重新復(fù)用這個(gè) ViewHolder谍椅,新位置的卡位無法從 mCachedViews 里拿 ViewHolder出來用。
找到 viewholder 后
就算 position 匹配找到了 ViewHolder古话,還需要判斷一下這個(gè) ViewHolder 是否已經(jīng)被 remove 掉雏吭,type 類型一致不一致,如下陪踩。
以上是在 mCachedViews 中尋找杖们,沒有找到的話,就繼續(xù)再找一遍膊毁,剛才是通過 position 來找胀莹,那這次就換成id,然后重復(fù)上面的步驟再找一遍婚温,如下
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ù)往下吕喘。
這個(gè)就是常說擴(kuò)展類了,RecyclerView 提供給我們自定義實(shí)現(xiàn)的擴(kuò)展類刑桑,我們可以重寫 getViewForPositionAndType() 方法來實(shí)現(xiàn)自己的復(fù)用策略氯质。不過,也沒用過祠斧,那這部分也當(dāng)作不會執(zhí)行闻察,略過。繼續(xù)往下
這里也是重點(diǎn)了琢锋,記筆記記筆記辕漂。
這里是去 RecyclerViewPool 里取 ViewHolder,ViewPool 會根據(jù)不同的 item type 創(chuàng)建不同的 List吴超,每個(gè) List 默認(rèn)大小為5個(gè)钉嘹。看一下去 ViewPool 里是怎么找的
之前說過烛芬,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ù)往下看
如果 ViewPool 中都沒有找到 ViewHolder 來使用的話隐砸,那就調(diào)用 Adapter 的 onCreateViewHolder 來創(chuàng)建一個(gè)新的 ViewHolder 使用。
上面一共有很多步驟來找 ViewHolder蝙眶,不管在哪個(gè)步驟季希,只要找到 ViewHolder 的話褪那,那下面那些步驟就不用管了,然后都要繼續(xù)往下判斷是否需要重新綁定數(shù)據(jù)式塌,還有檢查布局參數(shù)是否合法博敬。如下:
到這里,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í)的入口是:
本篇分析的滑動(dòng)場景论泛,在 RecyclerView 滑動(dòng)時(shí),會交由 LinearLayoutManager 的 scrollVerticallyBy() 去處理蛹屿,然后 LayoutManager 會接著調(diào)用 fill() 方法去處理需要復(fù)用和回收的卡位屁奏,最終會調(diào)用上述 recyclerView() 這個(gè)方法開始進(jìn)行回收工作。
本篇分析的滑動(dòng)場景错负,在 RecyclerView 滑動(dòng)時(shí)坟瓢,會交由 LinearLayoutManager 的 scrollVerticallyBy() 去處理,然后 LayoutManager 會接著調(diào)用 fill() 方法去處理需要復(fù)用和回收的卡位犹撒,最終會調(diào)用上述 recyclerView() 這個(gè)方法開始進(jìn)行回收工作折联。
回收的邏輯比較簡單,由 LayoutManager 來遍歷移出屏幕的卡位识颊,然后對每個(gè)卡位進(jìn)行回收操作诚镰,回收時(shí),都是把 ViewHolder 放在 mCachedViews 里面,如果 mCachedViews 滿了怕享,那就在 mCachedViews 里拿一個(gè) ViewHolder 扔到 ViewPool 緩存里执赡,然后 mCachedViews 就可以空出位置來放新回收的 ViewHolder 了。
總結(jié)一下:
RecyclerView 滑動(dòng)場景下的回收復(fù)用涉及到的結(jié)構(gòu)體兩個(gè):
mCachedViews
和 RecyclerViewPool
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ù)即可卿啡。
整體的流程圖如下:(可放大查看)
最后吟吝,解釋一下開頭的問題
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ù)用命中流程圖
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)畫
- 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中獲取
- 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緩存大小