基於滑動場景解析RecyclerView的回收複用機制原理

本篇文章已受權微信公衆號 guolin_blog (郭霖)獨家發佈android

最近在研究 RecyclerView 的回收複用機制,順便記錄一下。咱們知道,RecyclerView 在 layout 子 View 時,都經過回收複用機制來管理。網上關於回收複用機制的分析講解的文章也有一大堆了,分析得也都很詳細,什麼四級緩存啊,先去 mChangedScrap 取再去哪裏取啊之類的;但其實,我想說的是,RecyclerView 的回收複用機制確實很完善,覆蓋到各類場景中,但並非每種場景的回收複用時都會將機制的全部流程走一遍的。舉個例子說,在 setLayoutManager、setAdapter、notifyDataSetChanged 或者滑動時等等這些場景都會觸發回收複用機制的工做。可是若是隻是 RecyclerView 滑動的場景觸發的回收複用機制工做時,其實並不須要四級緩存都參與的。api

emmm,應該講得仍是有點懵,那就繼續看下去吧,會一點一點慢慢分析。本篇不會像其餘大神的文章同樣,把回收複用機制源碼一行行分析下來,我也沒那個能力,因此我會基於一種特定的場景來分析源碼,這樣會更容易理解的。廢話結束,開始正題。緩存

正題

RecyclerView 的回收複用機制的內部實現都是由 Recycler 內部類實現,下面就都以這樣一種頁面的滑動場景來說解 RecyclerView 的回收複用機制。微信

RecyclerView頁面.png

相應的版本: RecyclerView: recyclerview-v7-25.1.0.jar LayoutManager: GridLayoutManager extends LinearLayoutManager (recyclerview-v7-25.1.0.jar)源碼分析

這個頁面每行可顯示5個卡位,每一個卡位的 item 佈局 type 一致。佈局

開始分析回收複用機制以前,先提幾個問題:3d

Q1:若是向下滑動,新一行的5個卡位的顯示會去複用緩存的 ViewHolder,第一行的5個卡位會移出屏幕被回收,那麼在這個過程當中,是先進行復用再回收?仍是先回收再複用?仍是邊回收邊複用?也就是說,新一行的5個卡位複用的 ViewHolder 有多是第一行被回收的5個卡位嗎?

第二個問題以前,先看幾張圖片:日誌

先向下再向上滑動.png

黑框表示屏幕,RecyclerView 先向下滑動,第三行卡位顯示出來,再向上滑動,第三行移出屏幕,第一行顯示出來。咱們分別在 Adapter 的 onCreateViewHolder() 和 onBindViewHolder() 裏打日誌,下面是這個過程的日誌:cdn

日誌.png

紅框1是 RecyclerView 向下滑動操做的日誌,第三行5個卡位的顯示都是從新建立的 ViewHolder ;紅框2是再次向上滑動時的日誌,第一行5個卡位的從新顯示用的 ViewHolder 都是複用的,由於沒有 create viewHolder 的日誌,而後只有後面3個卡位從新綁定數據,調用了onBindViewHolder();那麼問題來了:xml

Q2: 在這個過程當中,爲何當 RecyclerView 再次向上滑動從新顯示第一行的5個卡位時,只有後面3個卡位觸發了 onBindViewHolder() 方法,從新綁定數據呢?明明5個卡位都是複用的。

在上面的操做基礎上,咱們繼續往下操做:

先向下再向下.png

在第二個問題操做的基礎上,目前已經建立了15個 ViewHolder,此時顯示的是第一、2行的卡位,那麼繼續向下滑動兩次,這個過程的日誌以下:

日誌.png

紅框1是第二個問題操做的日誌,在這裏截出來只是爲了顯示接下去的日誌是在上面的基礎上繼續操做的;

紅框2就是第一次向下滑時的日誌,對比問題2的日誌,此次第三行的5個卡位用的 ViewHolder 也都是複用的,並且也只有後面3個卡位觸發了 onBindViewHolder() 從新綁定數據;

紅框3是第二次向下滑動時的日誌,此次第四行的5個卡位,前3個的卡位用的 ViewHolder 是複用的,後面2個卡位的 ViewHolder 則是從新建立的,並且5個卡位都調用了 onBindViewHolder() 從新綁定數據;

那麼,

Q3:接下去不論是向上滑動仍是向下滑動,滑動幾回,都不會再有 onCreateViewHolder() 的日誌了,也就是說 RecyclerView 總共建立了17個 ViewHolder,但有時一行的5個卡位只有3個卡位須要從新綁定數據,有時卻又5個卡位都須要從新綁定數據,這是爲何呢?

若是明白 RecyclerView 的回收複用機制,那麼這三個問題也就都知道緣由了;反過來,若是知道這三個問題的緣由,那麼理解 RecyclerView 的回收複用機制也就更簡單了;因此,帶着問題,在特定的場景下去分析源碼的話,應該會比較容易。

源碼分析

其實,根據問題2的日誌,咱們就能夠回答問題1了。在目前顯示一、2行, ViewHolder 的個數爲10個的基礎上,第三行的5個新卡位要顯示出來都須要從新建立 ViewHolder,也就是說,在這個向下滑動的過程,是5個新卡位的複用機制先進行工做,而後第1行的5個被移出屏幕的卡位再進行回收機制工做。

那麼,就先來看看複用機制的源碼

複用機制

getViewForPosition()

Recycler.getViewForPosition()

Recycler.getViewForPosition()

Recycler.tryGetViewHolderForPositionByDeadline

這個方法是複用機制的入口,也就是 Recycler 開放給外部使用複用機制的api,外部調用這個方法就能夠返回想要的 View,而至於這個 View 是複用而來的,仍是從新建立得來的,就都由 Recycler 內部實現,對外隱藏。

tryGetViewHolderForPositionByDeadline()

因此,Recycler 的複用機制內部實現就在這個方法裏。 分析邏輯以前,先看一下 Recycler 的幾個結構體,用來緩存 ViewHolder 的。

Recycler

mAttachedScrap: 用於緩存顯示在屏幕上的 item 的 ViewHolder,場景好像是 RecyclerView 在 onLayout 時會先把 children 都移除掉,再從新添加進去,因此這個 List 應該是用在佈局過程當中臨時存放 children 的,反正在 RecyclerView 滑動過程當中不會在這裏面來找複用的 ViewHolder 就是了。

mChangedScrap: 這個沒理解是幹嗎用的,看名字應該跟 ViewHolder 的數據發生變化時有關吧,在 RecyclerView 滑動的過程當中,也沒有發現到這裏找複用的 ViewHolder,因此這個能夠先暫時放一邊。

**mCachedViews:**這個就重要得多了,滑動過程當中的回收和複用都是先處理的這個 List,這個集合裏存的 ViewHolder 的本來數據信息都在,因此能夠直接添加到 RecyclerView 中顯示,不須要再次從新 onBindViewHolder()。

mUnmodifiableAttachedScrap: 不清楚幹嗎用的,暫時跳過。

**mRecyclerPool:**這個也很重要,但存在這裏的 ViewHolder 的數據信息會被重置掉,至關於 ViewHolder 是一個重創新建的同樣,因此須要從新調用 onBindViewHolder 來綁定數據。

**mViewCacheExtension:**這個是留給咱們本身擴展的,好像也沒怎麼用,就暫時不分析了。

那麼接下去就看看複用的邏輯:

第1步

第一步很簡單,position 若是在 item 的範圍以外的話,那就拋異常吧。繼續往下看

第2步

若是是在 isPreLayout() 時,那麼就去 mChangedScrap 中找。 那麼這個 isPreLayout 表示的是什麼?,有兩個賦值的地方。

延伸

延伸

emmm,看樣子,在 LayoutManager 的 onLayoutChildren 前就會置爲 false,不過我仍是不懂這個過程是幹嗎的,滑動過程當中好像 mState.mInPreLayou = false,因此並不會來這裏,先暫時跳過。繼續往下。

第3步

跟進這個方法看看

第3.1步 getScrapOrHiddenOrCachedHolderForPosition()

首先,去 mAttachedScrap 中尋找 position 一致的 viewHolder,須要匹配一些條件,大體是這個 viewHolder 沒有被移除,是有效的之類的條件,知足就返回這個 viewHolder。

因此,這裏的關鍵就是要理解這個 mAttachedScrap 究竟是什麼,存的是哪些 ViewHolder。

一次遙控器按鍵的操做,無論有沒有發生滑動,都會致使 RecyclerView 的從新 onLayout,那要 layout 的話,RecyclerView 會先把全部 children 先 remove 掉,而後再從新 add 上去,完成一次 layout 的過程。那麼這暫時性的 remove 掉的 viewHolder 要存放在哪呢,就是放在這個 mAttachedScrap 中了,這就是個人理解了。

因此,感受這個 mAttachedScrap 中存放的 viewHolder 跟回收和複用關係不大。

網上一些分析的文章有說,RecyclerView 在複用時會按順序去 mChangedScrap, mAttachedScrap 等等緩存裏找,沒有找到再往下去找,從代碼上來看是這樣沒錯,但我以爲這樣表述有問題。由於就咱們這篇文章基於 RecyclerView 的滑動場景來講,新卡位的複用以及舊卡位的回收機制,其實都不會涉及到mChangedScrap 和 mAttachedScrap,因此我以爲仍是基於某種場景來分析相對應的回收複用機制會比較好。就像mChangedScrap 我雖然沒理解是幹嗎用的,但我猜想應該是在當數據發生變化時纔會涉及到的複用場景,因此當我分析基於滑動場景時的複用時,即便我對這塊不理解,影響也不會很大。

繼續往下看

第3.2步

emmm,這段也仍是沒看懂,但估計應該須要一些特定的場景下所使用的複用策略吧,看名字,應該跟 hidden 有關?不懂,跳過這段,應該也沒事,滑動過程當中的回收複用跟這個應該也關係不大。

第3.3步

這裏就要畫重點啦,記筆記記筆記,滑動場景中的複用會用到這裏的機制。

mCachedViews 的大小默認爲2。遍歷 mCachedViews,找到 position 一致的 ViewHolder,以前說過,mCachedViews 裏存放的 ViewHolder 的數據信息都保存着,因此 mCachedViews 能夠理解成,只有原來的卡位能夠從新複用這個 ViewHolder,新位置的卡位沒法從 mCachedViews 裏拿 ViewHolder出來用

找到 viewholder 後

第4步

就算 position 匹配找到了 ViewHolder,還須要判斷一下這個 ViewHolder 是否已經被 remove 掉,type 類型一致不一致,以下。

第4.1步

以上是在 mCachedViews 中尋找,沒有找到的話,就繼續再找一遍,剛纔是經過 position 來找,那此次就換成id,而後重複上面的步驟再找一遍,以下

第5步

getScrapOrCachedViewForId() 作的事跟 getScrapOrHiddenOrCacheHolderForPosition() 其實差很少,只不過一個是經過 position 來找 ViewHolder,一個是經過 id 來找。而這個 id 並非咱們在 xml 中設置的 android:id, 而是 Adapter 持有的一個屬性,默認是不會使用這個屬性的,因此這個第5步實際上是不會執行的,除非咱們重寫了 Adapter 的 setHasStableIds(),既然不是經常使用的場景,那就先略過吧,那就繼續往下。

第6步

這個就是常說擴展類了,RecyclerView 提供給咱們自定義實現的擴展類,咱們能夠重寫 getViewForPositionAndType() 方法來實現本身的複用策略。不過,也沒用過,那這部分也看成不會執行,略過。繼續往下

第7步

這裏也是重點了,記筆記記筆記。

這裏是去 RecyclerViewPool 裏取 ViewHolder,ViewPool 會根據不一樣的 item type 建立不一樣的 List,每一個 List 默認大小爲5個。看一下去 ViewPool 裏是怎麼找的

第7.1步

以前說過,ViewPool 會根據不一樣的 viewType 建立不一樣的集合來存放 ViewHolder,那麼複用的時候,只要 ViewPool 裏相同的 type 有 ViewHolder 緩存的話,就將最後一個拿出來複用,不用像 mCachedViews 須要各類匹配條件,只要有就能夠複用

繼續看"圖第7步"後面的代碼,拿到 ViewHolder 以後,還會再次調用 resetInternal() 來重置 ViewHolder,這樣 ViewHolder 就能夠看成一個全新的 ViewHolder 來使用了,這也就是爲何從這裏拿的 ViewHolder 都須要從新 onBindViewHolder() 了

那若是在 ViewPool 裏仍是沒有找到呢,繼續往下看

第8步

若是 ViewPool 中都沒有找到 ViewHolder 來使用的話,那就調用 Adapter 的 onCreateViewHolder 來建立一個新的 ViewHolder 使用。

上面一共有不少步驟來找 ViewHolder,無論在哪一個步驟,只要找到 ViewHolder 的話,那下面那些步驟就不用管了,而後都要繼續往下判斷是否須要從新綁定數據,還有檢查佈局參數是否合法。以下:

最後1步

到這裏,tryGetViewHolderForPositionByDeadline() 這個方法就結束了。這大概就是 RecyclerView 的複用機制,中間咱們跳過不少地方,由於 RecyclerView 有各類場景能夠刷新他的 view,好比從新 setLayoutManager(),從新 setAdapter(),或者 notifyDataSetChanged(),或者滑動等等之類的場景,只要從新layout,就會去回收和複用 ViewHolder,因此這個複用機制須要考慮到各類各樣的場景。

把代碼一行行的啃透有點吃力,因此我就只借助 RecyclerView 的滑動的這種場景來分析它涉及到的回收和複用機制。

下面就分析一下回收機制

回收機制

回收機制的入口就有不少了,由於 Recycler 有各類結構體,好比mAttachedScrap,mCachedViews 等等,不一樣結構體回收的時機都不同,入口也就多了。

因此,仍是基於 RecyclerView 的滑動場景下,移出屏幕的卡位回收時的入口是:

Recycler.recyclerView()

本篇分析的滑動場景,在 RecyclerView 滑動時,會交由 LinearLayoutManager 的 scrollVerticallyBy() 去處理,而後 LayoutManager 會接着調用 fill() 方法去處理須要複用和回收的卡位,最終會調用上述 recyclerView() 這個方法開始進行回收工做。

recycleViewHolderInternal()

上圖第一個紅框中的代碼

從 mCacheViews 中扔 ViewHolder 到 ViewPool中去

addViewHolderToRecycledViewPool

putRecycledView()

回收的邏輯比較簡單,由 LayoutManager 來遍歷移出屏幕的卡位,而後對每一個卡位進行回收操做,回收時,都是把 ViewHolder 放在 mCachedViews 裏面,若是 mCachedViews 滿了,那就在 mCachedViews 裏拿一個 ViewHolder 扔到 ViewPool 緩存裏,而後 mCachedViews 就能夠空出位置來放新回收的 ViewHolder 了。

總結一下:

RecyclerView 滑動場景下的回收複用涉及到的結構體兩個: mCachedViews 和 RecyclerViewPool

mCachedViews 優先級高於 RecyclerViewPool,回收時,最新的 ViewHolder 都是往 mCachedViews 裏放,若是它滿了,那就移出一個扔到 ViewPool 裏好空出位置來緩存最新的 ViewHolder。

複用時,也是先到 mCachedViews 裏找 ViewHolder,但須要各類匹配條件,歸納一下就是隻有原來位置的卡位能夠複用存在 mCachedViews 裏的 ViewHolder,若是 mCachedViews 裏沒有,那麼纔去 ViewPool 裏找。

在 ViewPool 裏的 ViewHolder 都是跟全新的 ViewHolder 同樣,只要 type 同樣,有找到,就能夠拿出來複用,從新綁定下數據便可。

總體的流程圖以下:(可放大查看)

滑動場景下的回收複用流程圖.png

最後,解釋一下開頭的問題

Q1:若是向下滑動,新一行的5個卡位的顯示會去複用緩存的 ViewHolder,第一行的5個卡位會移出屏幕被回收,那麼在這個過程當中,是先進行復用再回收?仍是先回收再複用?仍是邊回收邊複用?也就是說,新一行的5個卡位複用的 ViewHolder 有多是第一行被回收的5個卡位嗎?

答:先複用再回收,新一行的5個卡位先去目前的 mCachedViews 和 ViewPool 的緩存中尋找複用,沒有就從新建立,而後移出屏幕的那行的5個卡位再回收緩存到 mCachedViews 和 ViewPool 裏面,因此新一行5個卡位和複用不可能會用到剛移出屏幕的5個卡位。

Q2: 在這個過程當中,爲何當 RecyclerView 再次向上滑動從新顯示第一行的5個卡位時,只有後面3個卡位觸發了 onBindViewHolder() 方法,從新綁定數據呢?明明5個卡位都是複用的。

答:滑動場景下涉及到的回收和複用的結構體是 mCachedViews 和 ViewPool,前者默認大小爲2,後者爲5。因此,當第三行顯示出來後,第一行的5個卡位被回收,回收時先緩存在 mCachedViews,滿了再移出舊的到 ViewPool 裏,全部5個卡位有2個緩存在 mCachedViews 裏,3個緩存在 ViewPool,至因而哪2個緩存在 mCachedViews,這是由 LayoutManager 控制。

上面講解的例子使用的是 GridLayoutManager,滑動時的回收邏輯則是在父類 LinearLayoutManager 裏實現,回收第一行卡位時是從後往前回收,因此最新的兩個卡位是0、1,會放在 mCachedViews 裏,而二、三、4的卡位則放在 ViewPool 裏。

因此,當再次向上滑動時,第一行5個卡位會去兩個結構體裏找複用,以前說過,mCachedViews 裏存放的 ViewHolder 只有本來位置的卡位才能複用,因此0、1兩個卡位均可以直接去 mCachedViews 裏拿 ViewHolder 複用,並且這裏的 ViewHolder 是不用從新綁定數據的,至於二、三、4卡位則去 ViewPool 裏找,恰好 ViewPool 裏緩存着3個 ViewHolder,因此第一行的5個卡位都是用的複用的,而從 ViewPool 裏拿的複用須要從新綁定數據,纔會這樣只有三個卡位須要從新綁定數據。

Q3:接下去不論是向上滑動仍是向下滑動,滑動幾回,都不會再有 onCreateViewHolder() 的日誌了,也就是說 RecyclerView 總共建立了17個 ViewHolder,但有時一行的5個卡位只有3個卡位須要從新綁定數據,有時卻又5個卡位都須要從新綁定數據,這是爲何呢?

答:有時一行只有3個卡位須要從新綁定的緣由跟Q2同樣,由於 mCachedView 里正好緩存着當前位置的 ViewHolder,原本就是它的 ViewHolder 固然能夠直接拿來用。而至於爲何會建立了17個 ViewHolder,那是由於再第四行的卡位要顯示出來時,ViewPool 裏只有3個緩存,而第四行的卡位又用不了 mCachedViews 裏的2個緩存,由於這兩個緩存的是0、1卡位的 ViewHolder,因此就須要再從新建立2個 ViewHodler 來給第四行最後的兩個卡位使用。


QQ圖片20180316094923.jpg
最近剛開通了公衆號,想激勵本身堅持寫做下去,初期主要分享原創的Android或Android-Tv方面的小知識,感興趣的能夠點一波關注,謝謝支持~~
相關文章
相關標籤/搜索