網上一些分析的文章有說,RecyclerView 在複用時會按順序去 mChangedScrap, mAttachedScrap 等等緩存裡找,沒有找到再往下去找,從代碼上來看是這樣沒錯,但我覺得這樣表述有問題。因為就我們這篇文章基于 RecyclerView 的滑動場景來說,新卡位的複用以及舊卡位的回收機制,其實都不會涉及到mChangedScrap 和 mAttachedScrap,是以我覺得還是基于某種場景來分析相對應的回收複用機制會比較好。
本篇文章已授權微信公衆号 guolin_blog (郭霖)獨家釋出
最近在研究 RecyclerView 的回收複用機制,順便記錄一下。我們知道,RecyclerView 在 layout 子 View 時,都通過回收複用機制來管理。網上關于回收複用機制的分析講解的文章也有一大堆了,分析得也都很詳細,什麼四級緩存啊,先去 mChangedScrap 取再去哪裡取啊之類的;但其實,我想說的是,RecyclerView 的回收複用機制确實很完善,覆寫到各種場景中,但并不是每種場景的回收複用時都會将機制的所有流程走一遍的。舉個例子說,在 setLayoutManager、setAdapter、notifyDataSetChanged 或者滑動時等等這些場景都會觸發回收複用機制的工作。但是如果隻是 RecyclerView 滑動的場景觸發的回收複用機制工作時,其實并不需要四級緩存都參與的。
emmm,應該講得還是有點懵,那就繼續看下去吧,會一點一點慢慢分析。本篇不會像其他大神的文章一樣,把回收複用機制源碼一行行分析下來,我也沒那個能力,是以我會基于一種特定的場景來分析源碼,這樣會更容易了解的。廢話結束,開始正題。
正題
RecyclerView 的回收複用機制的内部實作都是由 Recycler 内部類實作,下面就都以這樣一種頁面的滑動場景來講解 RecyclerView 的回收複用機制。
相應的版本:
RecyclerView: recyclerview-v7-25.1.0.jar
LayoutManager: GridLayoutManager extends LinearLayoutManager (recyclerview-v7-25.1.0.jar)
這個頁面每行可顯示5個卡位,每個卡位的 item 布局 type 一緻。
開始分析回收複用機制之前,先提幾個問題:
Q1:如果向下滑動,新一行的5個卡位的顯示會去複用緩存的 ViewHolder,第一行的5個卡位會移出螢幕被回收,那麼在這個過程中,是先進行複用再回收?還是先回收再複用?還是邊回收邊複用?也就是說,新一行的5個卡位複用的 ViewHolder 有可能是第一行被回收的5個卡位嗎?
第二個問題之前,先看幾張圖檔:
黑框表示螢幕,RecyclerView 先向下滑動,第三行卡位顯示出來,再向上滑動,第三行移出螢幕,第一行顯示出來。我們分别在 Adapter 的 onCreateViewHolder() 和 onBindViewHolder() 裡打日志,下面是這個過程的日志:
紅框1是 RecyclerView 向下滑動操作的日志,第三行5個卡位的顯示都是重新建立的 ViewHolder ;紅框2是再次向上滑動時的日志,第一行5個卡位的重新顯示用的 ViewHolder 都是複用的,因為沒有 create viewHolder 的日志,然後隻有後面3個卡位重新綁定資料,調用了onBindViewHolder();那麼問題來了:
Q2: 在這個過程中,為什麼當 RecyclerView 再次向上滑動重新顯示第一行的5個卡位時,隻有後面3個卡位觸發了 onBindViewHolder() 方法,重新綁定資料呢?明明5個卡位都是複用的。
在上面的操作基礎上,我們繼續往下操作:
在第二個問題操作的基礎上,目前已經建立了15個 ViewHolder,此時顯示的是第1、2行的卡位,那麼繼續向下滑動兩次,這個過程的日志如下:
紅框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了。在目前顯示1、2行,
ViewHolder 的個數為10個的基礎上,第三行的5個新卡位要顯示出來都需要重新建立 ViewHolder,也就是說,在這個向下滑動的過程,是5個新卡位的複用機制先進行工作,然後第1行的5個被移出螢幕的卡位再進行回收機制工作。
那麼,就先來看看複用機制的源碼
複用機制
getViewForPosition()
這個方法是複用機制的入口,也就是 Recycler 開放給外部使用複用機制的api,外部調用這個方法就可以傳回想要的 View,而至于這個 View 是複用而來的,還是重新建立得來的,就都由 Recycler 内部實作,對外隐藏。
tryGetViewHolderForPositionByDeadline()
是以,Recycler 的複用機制内部實作就在這個方法裡。
分析邏輯之前,先看一下 Recycler 的幾個結構體,用來緩存 ViewHolder 的。
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:這個是留給我們自己擴充的,好像也沒怎麼用,就暫時不分析了。
那麼接下去就看看複用的邏輯:
第一步很簡單,position 如果在 item 的範圍之外的話,那就抛異常吧。繼續往下看
如果是在 isPreLayout() 時,那麼就去 mChangedScrap 中找。
那麼這個 isPreLayout 表示的是什麼?,有兩個指派的地方。
emmm,看樣子,在 LayoutManager 的 onLayoutChildren 前就會置為
false,不過我還是不懂這個過程是幹嘛的,滑動過程中好像
mState.mInPreLayou = false,是以并不會來這裡,先暫時跳過。繼續往下。
跟進這個方法看看
首先,去 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 我雖然沒了解是幹嘛用的,但我猜測應該是在當資料發生變化時才會涉及到的複用場景,是以當我分析基于滑動場景時的複用時,即使我對這塊不了解,影響也不會很大。
繼續往下看
emmm,這段也還是沒看懂,但估計應該需要一些特定的場景下所使用的複用政策吧,看名字,應該跟 hidden 有關?不懂,跳過這段,應該也沒事,滑動過程中的回收複用跟這個應該也關系不大。
這裡就要畫重點啦,記筆記記筆記,滑動場景中的複用會用到這裡的機制。
mCachedViews 的大小預設為2。周遊 mCachedViews,找到 position 一緻的 ViewHolder,之前說過,mCachedViews 裡存放的 ViewHolder 的資料資訊都儲存着,是以 mCachedViews 可以了解成,隻有原來的卡位可以重新複用這個 ViewHolder,新位置的卡位無法從 mCachedViews 裡拿 ViewHolder出來用。
找到 viewholder 後
就算 position 比對找到了 ViewHolder,還需要判斷一下這個 ViewHolder 是否已經被 remove 掉,type 類型一緻不一緻,如下。
以上是在 mCachedViews 中尋找,沒有找到的話,就繼續再找一遍,剛才是通過 position 來找,那這次就換成id,然後重複上面的步驟再找一遍,如下
getScrapOrCachedViewForId() 做的事跟 getScrapOrHiddenOrCacheHolderForPosition() 其實差不多,隻不過一個是通過 position 來找 ViewHolder,一個是通過 id 來找。而這個 id 并不是我們在 xml 中設定的 android:id, 而是 Adapter 持有的一個屬性,預設是不會使用這個屬性的,是以這個第5步其實是不會執行的,除非我們重寫了 Adapter 的 setHasStableIds(),既然不是常用的場景,那就先略過吧,那就繼續往下。
這個就是常說擴充類了,RecyclerView 提供給我們自定義實作的擴充類,我們可以重寫 getViewForPositionAndType() 方法來實作自己的複用政策。不過,也沒用過,那這部分也當作不會執行,略過。繼續往下
這裡也是重點了,記筆記記筆記。
這裡是去 RecyclerViewPool 裡取 ViewHolder,ViewPool 會根據不同的 item type 建立不同的 List,每個 List 預設大小為5個。看一下去 ViewPool 裡是怎麼找的
之前說過,ViewPool 會根據不同的 viewType 建立不同的集合來存放 ViewHolder,那麼複用的時候,隻要 ViewPool 裡相同的 type 有 ViewHolder 緩存的話,就将最後一個拿出來複用,不用像 mCachedViews 需要各種比對條件,隻要有就可以複用。
繼續看"圖第7步"後面的代碼,拿到 ViewHolder 之後,還會再次調用 resetInternal() 來重置 ViewHolder,這樣 ViewHolder 就可以當作一個全新的 ViewHolder 來使用了,這也就是為什麼從這裡拿的 ViewHolder 都需要重新 onBindViewHolder() 了。
那如果在 ViewPool 裡還是沒有找到呢,繼續往下看
如果 ViewPool 中都沒有找到 ViewHolder 來使用的話,那就調用 Adapter 的 onCreateViewHolder 來建立一個新的 ViewHolder 使用。
上面一共有很多步驟來找 ViewHolder,不管在哪個步驟,隻要找到 ViewHolder 的話,那下面那些步驟就不用管了,然後都要繼續往下判斷是否需要重新綁定資料,還有檢查布局參數是否合法。如下:
到這裡,tryGetViewHolderForPositionByDeadline() 這個方法就結束了。這大概就是 RecyclerView 的複用機制,中間我們跳過很多地方,因為 RecyclerView 有各種場景可以重新整理他的 view,比如重新 setLayoutManager(),重新 setAdapter(),或者 notifyDataSetChanged(),或者滑動等等之類的場景,隻要重新layout,就會去回收和複用 ViewHolder,是以這個複用機制需要考慮到各種各樣的場景。
把代碼一行行的啃透有點吃力,是以我就隻借助 RecyclerView 的滑動的這種場景來分析它涉及到的回收和複用機制。
下面就分析一下回收機制
回收機制
回收機制的入口就有很多了,因為 Recycler 有各種結構體,比如mAttachedScrap,mCachedViews 等等,不同結構體回收的時機都不一樣,入口也就多了。
是以,還是基于 RecyclerView 的滑動場景下,移出螢幕的卡位回收時的入口是:
本篇分析的滑動場景,在 RecyclerView 滑動時,會交由 LinearLayoutManager 的 scrollVerticallyBy() 去處理,然後 LayoutManager 會接着調用 fill() 方法去處理需要複用和回收的卡位,最終會調用上述 recyclerView() 這個方法開始進行回收工作。
回收的邏輯比較簡單,由 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 一樣,有找到,就可以拿出來複用,重新綁定下資料即可。
整體的流程圖如下:(可放大檢視)
最後,解釋一下開頭的問題
答:先複用再回收,新一行的5個卡位先去目前的 mCachedViews 和 ViewPool 的緩存中尋找複用,沒有就重新建立,然後移出螢幕的那行的5個卡位再回收緩存到 mCachedViews 和 ViewPool 裡面,是以新一行5個卡位和複用不可能會用到剛移出螢幕的5個卡位。
答:滑動場景下涉及到的回收和複用的結構體是 mCachedViews 和 ViewPool,前者預設大小為2,後者為5。是以,當第三行顯示出來後,第一行的5個卡位被回收,回收時先緩存在 mCachedViews,滿了再移出舊的到 ViewPool 裡,所有5個卡位有2個緩存在 mCachedViews 裡,3個緩存在 ViewPool,至于是哪2個緩存在 mCachedViews,這是由 LayoutManager 控制。
上面講解的例子使用的是 GridLayoutManager,滑動時的回收邏輯則是在父類 LinearLayoutManager 裡實作,回收第一行卡位時是從後往前回收,是以最新的兩個卡位是0、1,會放在 mCachedViews 裡,而2、3、4的卡位則放在 ViewPool 裡。
是以,當再次向上滑動時,第一行5個卡位會去兩個結構體裡找複用,之前說過,mCachedViews 裡存放的 ViewHolder 隻有原本位置的卡位才能複用,是以0、1兩個卡位都可以直接去 mCachedViews 裡拿 ViewHolder 複用,而且這裡的 ViewHolder 是不用重新綁定資料的,至于2、3、4卡位則去 ViewPool 裡找,剛好 ViewPool 裡緩存着3個 ViewHolder,是以第一行的5個卡位都是用的複用的,而從 ViewPool 裡拿的複用需要重新綁定資料,才會這樣隻有三個卡位需要重新綁定資料。
答:有時一行隻有3個卡位需要重新綁定的原因跟Q2一樣,因為 mCachedView 裡正好緩存着目前位置的 ViewHolder,本來就是它的 ViewHolder 當然可以直接拿來用。而至于為什麼會建立了17個 ViewHolder,那是因為再第四行的卡位要顯示出來時,ViewPool 裡隻有3個緩存,而第四行的卡位又用不了 mCachedViews 裡的2個緩存,因為這兩個緩存的是6、7卡位的 ViewHolder,是以就需要再重新建立2個 ViewHodler 來給第四行最後的兩個卡位使用。
最近剛開通了公衆号,想激勵自己堅持寫作下去,初期主要分享原創的Android或Android-Tv方面的小知識,感興趣的可以點一波關注,謝謝支援~~