RecyclerView快取原理,有圖有真相

尋找極限的貓發表於2018-08-19

1. RecyclerView快取機制與效能優化關係

RecyclerView做效能優化要說複雜也複雜,比如說佈局優化,快取,預載入等等。其優化的點很多,在這些看似獨立的點之間,其實存在一個樞紐:Adapter。因為所有的ViewHolder的建立和內容的繫結都需要經過Adaper的兩個函式onCreateViewHolder和onBindViewHolder

因此我們效能優化的本質就是要減少這兩個函式的呼叫時間和呼叫的次數。如果我們想對RecyclerView做效能優化,必須清楚的瞭解到我們的每一步操作背後,onCreateViewHolder和onBindViewHolder呼叫了多少次。因此,瞭解RecyclerView的快取機制是RecyclerView效能優化的基礎。

為了理解快取的應用場景,本文首先會簡單介紹一下RecyclerView的繪製原理,然後再分析其快取實現原理。

這裡寫圖片描述

2. 繪製原理簡述

2.1 假設

為了簡化問題,繪製原理介紹提供以下假設:

  • RecyclerView
    • 以LinearLayoutManager為例
    • 忽略ItemDecoration
    • 忽略ItemAnimator
    • 忽略Measure過程
    • 假設RecyclerView的width和height是確定的
  • Recycler
    • 忽略mViewCacheExtension

2.2 繪製過程

(1)類的職責介紹

LayoutManager:接管RecyclerView的Measure,Layout,Draw的過程

Recycler:快取池

Adapter:ViewHolder的生成器和內容繫結器。

(2)繪製過程簡介

  1. RecyclerView.requestLayout開始發生繪製,忽略Measure的過程
  2. 在Layout的過程會通過LayoutManager.fill去將RecyclerView填滿
  3. LayoutManager.fill會呼叫LayoutManager.layoutChunk去生成一個具體的ViewHolder
  4. 然後LayoutManager就會呼叫Recycler.getViewForPosition向Recycler去要ViewHolder
  5. Recycler首先去一級快取(Cache)裡面查詢是否命中,如果命中直接返回。如果一級快取沒有找到,則去三級快取查詢,如果三級快取找到了則呼叫Adapter.bindViewHolder來繫結內容,然後返回。如果三級快取沒有找到,那麼就通過Adapter.createViewHolder建立一個ViewHolder,然後呼叫Adapter.bindViewHolder繫結其內容,然後返回為Recycler。【參見後文:2. 快取機制】
  6. 一直重複步驟3-5,知道建立的ViewHolder填滿了整個RecyclerView為止。
RecyclerView快取原理,有圖有真相

3. 快取機制

3.1 原始碼簡析

RecyclerView在Recyler裡面實現ViewHolder的快取,Recycler裡面的實現快取的主要包含以下5個物件:

  • ArrayList mAttachedScrap:未與RecyclerView分離的ViewHolder列表,如果仍依賴於 RecyclerView (比如已經滑動出可視範圍,但還沒有被移除掉),但已經被標記移除的 ItemView 集合會被新增到 mAttachedScrap 中
    • 按照id和position來查詢ViewHolder
  • ArrayList mChangedScrap:表示資料已經改變的viewHolder列表,儲存 notifXXX 方法時需要改變的 ViewHolder,匹配機制按照position和id進行匹配
  • ArrayList mCachedViews:快取ViewHolder,主要用於解決RecyclerView滑動抖動時的情況,還有用於儲存Prefetch的ViewHoder
    • 最大的數量為:mViewCacheMax = mRequestedCacheMax + extraCache(extraCache是由prefetch的時候計算出來的)
  • ViewCacheExtension mViewCacheExtension:開發者可自定義的一層快取,是虛擬類ViewCacheExtension的一個例項,開發者可實現方法getViewForPositionAndType(Recycler recycler, int position, int type)來實現自己的快取。
  • mRecyclerPool ViewHolder快取池,在有限的mCachedViews中如果存不下ViewHolder時,就會把ViewHolder存入RecyclerViewPool中。
    • 按照Type來查詢ViewHolder
    • 每個Type預設最多快取5個
public final class Recycler {
    final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
    ArrayList<ViewHolder> mChangedScrap = null;

    final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();

    private final List<ViewHolder>
            mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);

    private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
    int mViewCacheMax = DEFAULT_CACHE_SIZE;

    RecycledViewPool mRecyclerPool;

    private ViewCacheExtension mViewCacheExtension;

    static final int DEFAULT_CACHE_SIZE = 2;
複製程式碼

3.2 快取機制圖解

RecyclerView在設計的時候講上述5個快取物件分為了3級。每次建立ViewHolder的時候,會按照優先順序依次查詢快取建立ViewHolder。每次講ViewHolder快取到Recycler快取的時候,也會按照優先順序依次快取進去。三級快取分別是:

  • 一級快取:返回佈局和內容都都有效的ViewHolder
    • 按照position或者id進行匹配
    • 命中一級快取無需onCreateViewHolder和onBindViewHolder
    • mAttachScrap在adapter.notifyXxx的時候用到
    • mChanedScarp在每次View繪製的時候用到,因為getViewHolderForPosition非呼叫多次,後面將
    • mCachedView:用來解決滑動抖動的情況,預設值為2
  • 二級快取:返回View
    • 按照position和type進行匹配
    • 直接返回View
    • 需要自己繼承ViewCacheExtension實現
    • 位置固定,內容不發生改變的情況,比如說Header如果內容固定,就可以使用
  • 三級快取:返回佈局有效,內容無效的ViewHolder
    • 按照type進行匹配,每個type快取值預設=5
    • layout是有效的,但是內容是無效的
    • 多個RecycleView可共享,可用於多個RecyclerView的優化 RecyclerView快取原理,有圖有真相

3.3 例項講解

這裡寫圖片描述

例項解釋:

(1)由於ViewCacheExtension在實際使用的時候較少用到,因此本例中忽略二級快取

(2)mChangedScrap和mAttchScrap是RecyclerView內部控制的快取,本例暫時忽略。

(3)為了簡化問題,暫時不考慮Pretch的情況

(4)圖片解釋:

  • RecyclerView包含三部分:已經出螢幕,在螢幕裡面,即將進入螢幕,我們滑動的方向是向上
  • RecyclerView包含三種Type:1,2,3。螢幕裡面的都是Type=3
  • 紅色的線代表已經出螢幕的ViewHoder與Recycler的互動情況
  • 綠色的線代表,即將進入螢幕的ViewHoder進入螢幕時候,ViewHolder與Recycler的互動情況

出螢幕時候的情況

  1. 當ViewHolder(position=0,type=1)出螢幕的時候,由於mCacheViews是空的,那麼就直接放在mCacheViews裡面,ViewHolder在mCacheViews裡面佈局和內容都是有效的,因此可以直接複用。
  2. ViewHolder(position=1,type=2)同步驟1
  3. 當ViewHolder(position=2,type=1)出螢幕的時候由於一級快取mCacheViews已經滿了,因此將其放入RecyclerPool(type=1)的快取池裡面。此時ViewHolder的內容會被標記為無效,當其複用的時候需要再次通過Adapter.bindViewHolder來繫結內容。
  4. ViewHolder(position=3,type=2)同步驟3

進螢幕時候的情況

  1. 當ViewHolder(position=3-10,type=3)進入螢幕繪製的時候,由於Recycler的mCacheViews裡面找不到position匹配的View,同時RecyclerPool裡面找不到type匹配的View,因此,其只能通過adapter.createViewHolder來建立ViewHolder,然後通過adapter.bindViewHolder來繫結內容。
  2. 當ViewHolder(position=11,type=1)進入螢幕的時候,發現ReccylerPool裡面能找到type=1的快取,因此直接從ReccylerPool裡面取來使用。由於內容是無效的,因此還需要呼叫bindViewHolder來繫結佈局。同時ViewHolder(position=4,type=3)需要出螢幕,其直接進入RecyclerPool(type=3)的快取池中
  3. ViewHolder(position=12,type=2)同步驟6

螢幕往下拉ViewHoder(position=1)進入螢幕的情況

  1. 由於mCacheView裡面的有position=1的ViewHolder與之匹配,直接返回。由於內容是有效的,因此無需再次繫結內容
  2. ViewHolder(position=0)同步驟8

4. RecyclerView效能優化方向總結

這裡寫圖片描述

相關文章