tft每日頭條

 > 生活

 > recyclerview實現内容自适應

recyclerview實現内容自适應

生活 更新时间:2024-12-14 02:31:52

使用 ScrollView 的時候,它的所有子 view 都會一次性被加載出來。而正确使用 RecyclerView 可以做到按需加載,按需綁定,并實現複用。本文主要分析 RecyclerView 緩存複用的原理。

從緩存獲取 ViewHolder 流程概覽

從緩存獲取的大緻流程如下圖所示:

recyclerview實現内容自适應(深入理解RecyclerView的緩存機制)1

說明:

在創建 ViewHolder 之前,RecyclerView 會先從緩存中嘗試獲取是否有符合要求的 ViewHolder,詳見 Recycler#tryGetViewHolderForPositionByDeadline 方法

  • 第一次,嘗試從 mChangedScrap 中獲取。
    • 隻有在 mState.isPreLayout() 為 true 時,也就是預布局階段,才會做這次嘗試。
    • 「預布局」的概念會在介紹。
  • 第二次,getScrapOrHiddenOrCachedHolderForPosition() 獲得 ViewHolder。
    • 其中 mAttachedScrap 和 mCachedViews 都是 Recycler 的成員變量
    • 如果成功獲得 ViewHolder 則檢驗其有效性,
    • 檢驗失敗則将其回收 到 RecyclerViewPool 中
    • 檢驗成功可以直接使用
    • 嘗試從 1. mAttachedScrap 2.mHiddenViews 3.mCachedViews 中查找 ViewHolder
  • 第三次,如果給 Adapter 設置了 stableId,調用 getScrapOrCachedViewForId 嘗試獲取 ViewHolder。
    • 跟第二次的區别在于,之前是根據 position 查找,現在是根據 id 查找
  • 第四次,mViewCacheExtension 不為空的話,則調用 ViewCacheExtension#getViewForPositionAndType 方法嘗試獲取 View
    • 注:ViewCacheExtension 是由開發者設置的,默認情況下為空,一般我們也不會設置。這層緩存大部分情況下可以忽略。
  • 第五次。嘗試從 RecyclerViewPool 中獲取,相比較于 mCachedViews,從 mRecyclerPool 中成功獲取 ViewHolder 對象後并沒有做合法性和 item 位置校驗,隻檢驗 viewType 是否一緻。
    • 從 RecyclerViewPool 中取出來的 ViewHolder 需要重新執行 bind 才能使用。
  • 如果上面五次嘗試都失敗了,調用 RecyclerView.Adapter#createViewHolder 創建一個新的 ViewHolder
  • 最後根據 ViewHolder 的狀态,确定是否需要調用 bindViewHolder 進行數據綁定。

問題

預布局、預測動畫是什麼?

理解「預布局」需要先了解「預測動畫」。考慮這樣一個場景:

用戶有 A、B、C 三個 item,A,B 剛好顯示在屏幕中,這個時候,用戶把 B 删除了,那麼最終 C 會顯示在 B 原來的位置

recyclerview實現内容自适應(深入理解RecyclerView的緩存機制)2

如果 C 從底部平滑地滑動到之前 B 的位置将會更符合直覺。但是要做到這點實際上沒那麼簡單。因為我們隻知道 C 最終的位置,但是不知道 C 的起始位置在哪裡,無法确定 C 應該從哪裡滑動過來。如果根據最終的狀态,就斷定 C 應該要從底部滑動過來的話,很可能是有問題的。因為在其他 LayoutManager 中,它可能是從側面或者是其他地方滑動過來的。

那根據原狀态與最終狀态之間的差異,能不能得出我們應該執行什麼樣的切換動畫呢?答案依然是 no。因為在原狀态中,C 根本就不存在。(這個時候,我們并不知道,B 要被删除了,如果把 C 給加載出來,很可能是一種資源浪費。)

設計 RecyclerView 的工程師是這麼解決的。當 Adapter 發生變化的時候,RecyclerView 會讓 LayoutManager 進行兩次布局。

  • 第一次是 預布局 。将之前原狀态 下的 item 都布局出來。并且根據 Adapter 的 notify 信息,我們知道哪些 item 即将變化了,所以可以 加載出另外的 View 。在上述例子中,因為知道 B 已經被删除了,所以可以 把屏幕之外的 C 也加載出來
  • 第二個,最終的布局,也就是變化完成之後的布局。

recyclerview實現内容自适應(深入理解RecyclerView的緩存機制)3

這樣隻要比較前後布局的變化,就能得出應該執行什麼動畫了。

這種負責執行動畫的 view 在原布局或新布局中不存在的動畫,就稱為 預測動畫

預布局是實現預測動畫的一個步驟。

下面兩個動圖展示了普通動畫與預測動畫效果的區别:

普通動畫 :point_down:

recyclerview實現内容自适應(深入理解RecyclerView的緩存機制)4

預測動畫 :point_down:

recyclerview實現内容自适應(深入理解RecyclerView的緩存機制)5

關于預測動畫,感興趣的同學可以進一步閱讀這篇文章。

關于 Scrap

Scrap 緩存列表(mChangedScrap、mAttachedScrap)是 RecyclerView 最先查找 ViewHolder 地方,它跟 RecyclerViewPool 或者 ViewCache 有很大的區别。

mChangedScrap 和 mAttachedScrap 隻在布局階段使用。其他時候它們是空的。布局完成之後,這兩個緩存中的 viewHolder,會移到 mCacheView 或者 RecyclerViewPool 中。

當 LayoutManager 開始布局的時候(預布局或者是最終布局),當前布局中的所有 view,都會被 dump 到 scrap 中(具體實現可見 LinearLayoutManager#onLayoutChildren() 方法中調用了 detachAndScrapAttachedViews() ),然後 LayoutManager 挨個地取回 view,除非 view 發生了什麼變化,否則它會馬上從 scrap 中回到原來的位置。

recyclerview實現内容自适應(深入理解RecyclerView的緩存機制)6

img

以上圖為例,我們删除掉 b,調用 notifyItemRemove 方法,觸發重新布局,這時 a,b,c 都會被 dump 到 scrap 中,然後 LayoutManager 會從 scrap 中取回 a 和 c。

偏個題,這個時候,b 去哪了?RecyclerView 看到 b 沒有出現在最終的布局中,會 unscrap 它,讓它執行一個消失的動畫然後隐藏。動畫執行完之後,b 被放到 RecyclerViewPool 中。

為什麼 LayoutManager 需要先執行 detach,然後再重新 attach 這些 view,而不是隻移除哪些變化的子 view 呢?Scrap 緩存列表的存在,是為了隔離 LayoutManager 和 RecyclerView.Recycler 之間的關注點/職責。LayoutManager 不需要知道哪一個子 view 應該保留 或者是 應該被回收到 pool 亦或者其他什麼地方。這是 Recycler 的職責。

除了在布局時不為空外,還有另一個與 scrap 有關的規律:所有 scrap 的 view 都會跟 RecyclerView 分離。ViewGroup 中的 attachView 和 detachView 方法跟 addView 和 removeView 方法很像,但是不會觸發請求布局會重繪的事件。它們隻是從 ViewGroup 的子 view 列表中删除對應的子 view,并将該子 view 的 parent 設置為 null。detached 狀态必須是臨時,後面緊随着 attach 或者 remove 事件

如果在計算一個新布局的時候,已經添加了一堆子 view,可以放心的将它們全部 detach ,Recyclerview 就是這麼做的。

Attached vs Changed scrap

Recycler 類中,我們可以看到兩個單獨的 scrap 容器: mAttachedScrap 和 mChangedScrap。為什麼需要兩個呢?

ViewHolder 隻有在滿足下面情況才會被添加到 mChangedScrap:當它關聯的 item 發生了變化(notifyItemChanged 或者 notifyItemRangeChanged 被調用),并且 ItemAnimator 調用 ViewHolder#canReuseUpdatedViewHolder 方法時,返回了 false。否則,ViewHolder 會被添加到AttachedScrap 中。

canReuseUpdatedViewHolder 返回 “false” 表示我們要執行用一個 view 替換另一個 view 的動畫,例如淡入淡出動畫。“true”表示動畫在 view 内部發生。

mAttachedScrap 在 整個布局過程中都能使用,但是 changed scrap — 隻能在預布局階段使用。

這是有道理的:在布局後,新的 ViewHolder 應該替換掉“改變了的”視圖,因此 AttachedScrap 在布局後是沒有用的。更改動畫執行完成後,change scrap 将按預期方式轉存到 pool 中

默認的 ItemAnimator 可以在 3 種情況下重用更新的 ViewHolder:

  • 調用了 setSupportsChangeAnimations(false)。
  • 調用了 notifyDataSetChanged 而不是 notifyItemChanged 或 notifyItemRangeChanged 。
  • 提供了這樣的更改 payload:adapter.notifyItemChanged(index,anyObject)。

最後一種情況顯示了一種很好的方法,當隻想更改一些内部元素時,可以避免創建/綁定新的 ViewHolder。

Hidden Views 是什麼?

前面提到在第二次嘗試獲取 ViewHolder 的時候,有一個子步驟會從 hidden view 中搜索,這裡的 hidden view 指的是什麼?「hidden view」指的是那些正在從 RecyclerView 邊界中脫離的 view。為了讓這些 view 正确地執行對應的分離動畫,它們仍然作為 RecyclerView 的子 view 被保留下來。

站在 LayoutManager 的角度,這些 view 已經不存在了,因此不應該被包含在計算裡面。比如 在部分 view 正在執行消失動畫的過程中,調用 LayoutManager#getChildAt 方法,這些 view 不算在下标裡面。來自 LayoutManager 的所有對 getChildAt()、getChildCount()、addView() 等的方法調用 在應用到實際的可回收view 之前,都要通過 ChildHelper 處理,ChildHelper 的職責是重新計算非隐藏的子 view 列表和完整的子 view 列表之間的索引。

請記住,我們正在搜索要提供給 LayoutManager 的視圖, 但是 LayoutManager 不應了解隐藏 View

舉一個實際的:chestnut::這種讓人費解的“從隐藏的 view 彈跳”(bouncing from hidden views)機制對于處理下面這種情況而言是很有必要的。考慮這種場景,我們插入一個 item ,然後在插入動畫完成之前,馬上删除該 item:

recyclerview實現内容自适應(深入理解RecyclerView的緩存機制)7

我們想要看到的是 b 從 c 移除時的位置開始向上平移。但是在那個時候,b 是一個隐藏的 view!如果我們忽略了它(“隐藏”的 b),那會導緻在現有 b 下面創建一個新的 b。更糟糕的是,這兩個 view 會重疊,因為 新的 b 會往上,舊的 b 會往下。為了避免這種錯誤,在搜索 ViewHolder 的較早步驟之一中,RecyclerView 會詢問 ChildHelper 是否具有合适的 hidden view。所謂「合适」,表示這個 view 跟我們需要的位置相關聯,并具有正确的 view type,并且這個 view 的被隐藏的原因不是為了移除掉它(我們不應該讓被移除的 view 複活)

如果有這樣的 view ,RecyclerView 會将其返回到 LayoutManager 并将其添加到 preLayout 中以标記應從其進行動畫處理的位置(詳見 recordAnimationInfoIfBouncedHiddenView 方法)。

什麼?在 布局前後 添加内容不應該是 LayoutManager 的職責嗎?怎麼現在 RecyclerView 也在往 preLayout 中添加view?是的,這種機制看起來有點職責部分,但這是也說明我們有必要了解它。

Stable Id 的作用是什麼?

理解 stable Id 特性的最重要的一個點是,它隻會在調用 notifyDataSetChanged 方法之後,影響 RecyclerView 的行為。

如果調用 notifyDataSetChanged 的時候,Adapter 并沒有設置 hasStableId,RecyclerView 不知道 發生了什麼,哪一些東西變化了,所以,它假設所有的東西都變了,每一個 ViewHolder 都是無效的,因此應該把它們放到 RecyclerViewPool 而不是 scrap 中。

recyclerview實現内容自适應(深入理解RecyclerView的緩存機制)8

img

如果有 Stable Id,那那将會是像下面這樣:

recyclerview實現内容自适應(深入理解RecyclerView的緩存機制)9

img

ViewHolder 會進入 scrap 而不是 pool 中。然後會通過特定的 Id(Adapter 中的 getItemId 獲取到的 id)而不是 postion 到 scrap 中查找 ViewHolder。

好處是什麼?

  1. 不會導緻 RecyclerViewPool 溢出,因此非必須情況下,不需要創建新的 ViewHolder。之前的 ViewHolder 會重新綁定,因為 Id 沒有變化不代表内容沒有變化
  2. 最大好處的好處是 支持動畫。上面移動 item4 到 item6 的位置。正常情況下,我們需要調用 notifyItemMoved(4,6) 才能得到一個移動動畫。但是通過 stable id,調用 notifyDataSetChanged 也能支持這一點。因為 RecyclerView 可以看到特定 id 的 view 在新舊布局的上的位置,
  3. 要注意的是,這裡的動畫隻支持簡單的動畫,預測動畫無法支持。如果我們在新布局中看到一些 ID,而在舊布局中沒有,那麼我們如何知道它是新插入的 item 還是從某處移入的 item,在後一種情況下它究竟是從哪裡來的呢?通常,這些問題的答案會在預布局中找到,根據适配器的更改,該布局已超出 RecyclerView 的範圍,但現在這種情況下, 我們不知道這些更改具體是什麼

總體而言,stable id 的使用場景似乎比較有限。不過,還是有這樣一個使用場景:如果是從 ListView 遷移到 RecyclerView,将所有 notifyDataSetChanged 調用,都轉換為特定更改的通知可能會很痛苦。在這種情況下,stable id 可以提供給你提供簡單的 RecyclerView 動畫。

緩存優化實踐

  • 盡量使用 notifyItemXxx 方法進行細粒度的通知更新,而不是 notifyDatasetChanged
    • 如果變更前後是兩個數據集,無法确定具體哪一些數據項變化了,可以考慮使用 DiffUtil 。
    • 如果數據集較大,建議結合使用 AsyncListDiffer 在子線程做 diff 運算。
  • 如果特定 viewType 的 item 隻有一個,可以通過 RecyclerView#getRecycledViewPool()#setMaxRecycledViews(viewType,1); 來調整緩存區的大小,減少内存占用
  • 如果特定 viewType 的 item 特别多,但是不得不通過 notifyDataSetChange 方法更新數據,可以通過下面這種方式,在變更前調大緩存,變更完成後,調小緩存。這樣布局變化也可以最大程度地複用已有的 ViewHolder。mRecyclerView.getRecycledViewPool().setMaxRecycledViews(0, 屏幕顯示的item總數 7 );mAdapter.notifyDataSetChanged();new Handler().post(new Runnable() { @Override public void run() { mRecyclerView.getRecycledViewPool() .setMaxRecycledViews(0, 5); }});複制代碼
  • 如果 RecyclerView 中的每個 item 都是一個 RecyclerView, 并且子 RecyclerView 的 item type 相同可以通過 RecyclerView#setRecycledViewPool(); 方法,實現緩存池的複用。
,

更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!

查看全部

相关生活资讯推荐

热门生活资讯推荐

网友关注

Copyright 2023-2024 - www.tftnews.com All Rights Reserved