Android深入理解RecyclerView的緩存機制

RecyclerView在項目中的使用已經很普遍了,可以說是項目中最高頻使用的一個控件了。除了佈局靈活性、豐富的動畫,RecyclerView還有優秀的緩存機制,本文嘗試通過源碼深入瞭解一下RecyclerView中的緩存機制。

寫在前面

RecyclerView是通過內部類Recycler管理的緩存,那麼Recycler中緩存的是什麼?我們知道RecyclerView在存在大量數據時依然可以滑動的如絲滑般順暢,而RecyclerView本身是一個ViewGroup,那麼滑動時避免不了添加或移除子View(子View通過RecyclerView#Adapter中的onCreateViewHolder創建),如果每次使用子View都要去重新創建,肯定會影響滑動的流 暢性,所以RecyclerView通過Recycler來緩存的是ViewHolder(內部包含子View),這樣在滑動時可以複用子View,某些條件下還可以複用子View綁定的數據。所以本質上緩存是爲了減少重複繪製View和綁定數據的時間,從而提高了滑動時的性能

四級緩存

Recycler緩存ViewHolder對象有4個等級,優先級從高到底依次爲:

  • ArrayList<ViewHolder> mAttachedScrap
  • ArrayList<ViewHolder> mCachedViews
  • ViewCacheExtension mViewCacheExtension
  • RecycledViewPool mRecyclerPool

注:官網上貌似把mAttachedScrap、mCachedViews當成一級了,爲了方便區分,本文還是把他們當成兩級緩存。

緩存 涉及對象 作用 重新創建視圖View(onCreateViewHolder) 重新綁定數據(onBindViewHolder)
一級緩存 mAttachedScrap 緩存屏幕中可見範圍的ViewHolder false false
二級緩存 mCachedViews 緩存滑動時即將與RecyclerView分離的ViewHolder,按子View的position或id緩存,默認最多存放2個 false false
三級緩存 mViewCacheExtension 開發者自行實現的緩存 - -
四級緩存 mRecyclerPool ViewHolder緩存池,本質上是一個SparseArray,其中key是ViewType(int類型),value存放的是 ArrayList< ViewHolder>,默認每個ArrayList中最多存放5個ViewHolder false true

RecyclerView滑動時會觸發onTouchEvent#onMove,回收及複用ViewHolder在這裏就會開始。我們知道設置RecyclerView時需要設置LayoutManager,LayoutManager負責RecyclerView的佈局,包含對ItemView的獲取與複用。以LinearLayoutManager爲例,當RecyclerView重新佈局時會依次執行下面幾個方法:

  • onLayoutChildren():對RecyclerView進行佈局的入口方法
  • fill(): 負責對剩餘空間不斷地填充,調用的方法是layoutChunk()
  • layoutChunk():負責填充View,該View最終是通過在緩存類Recycler中找到合適的View的

上述的整個調用鏈:onLayoutChildren()->fill()->layoutChunk()->next()->getViewForPosition(),getViewForPosition()即是是從RecyclerView的回收機制實現類Recycler中獲取合適的View,下面主要就來從看這個Recycler#getViewForPosition()的實現。

@NonNull
public View getViewForPosition(int position) {
    return getViewForPosition(position, false);
}

View getViewForPosition(int position, boolean dryRun) {
    return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}

他們都會執行tryGetViewHolderForPositionByDeadline函數,繼續跟進去:

//根據傳入的position獲取ViewHolder 
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
        boolean dryRun, long deadlineNs) {
    ---------省略----------
    boolean fromScrapOrHiddenOrCache = false;
    ViewHolder holder = null;
    //預佈局 屬於特殊情況 從mChangedScrap中獲取ViewHolder
    if (mState.isPreLayout()) {
        holder = getChangedScrapViewForPosition(position);
        fromScrapOrHiddenOrCache = holder != null;
    }
    if (holder == null) {
        //1、嘗試從mAttachedScrap中獲取ViewHolder,此時獲取的是屏幕中可見範圍中的ViewHolder
        //2、mAttachedScrap緩存中沒有的話,繼續從mCachedViews嘗試獲取ViewHolder
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
     ----------省略----------
    }
    if (holder == null) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        ---------省略----------
        final int type = mAdapter.getItemViewType(offsetPosition);
        //如果Adapter中聲明瞭Id,嘗試從id中獲取,這裏不屬於緩存
        if (mAdapter.hasStableIds()) {
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                    type, dryRun);
        }
        if (holder == null && mViewCacheExtension != null) {
            3、從自定義緩存mViewCacheExtension中嘗試獲取ViewHolder,該緩存需要開發者實現
            final View view = mViewCacheExtension
                    .getViewForPositionAndType(this, position, type);
            if (view != null) {
                holder = getChildViewHolder(view);
            }
        }
        if (holder == null) { // fallback to pool
            //4、從緩存池mRecyclerPool中嘗試獲取ViewHolder
            holder = getRecycledViewPool().getRecycledView(type);
            if (holder != null) {
                //如果獲取成功,會重置ViewHolder狀態,所以需要重新執行Adapter#onBindViewHolder綁定數據
                holder.resetInternal();
                if (FORCE_INVALIDATE_DISPLAY_LIST) {
                    invalidateDisplayListInt(holder);
                }
            }
        }
        if (holder == null) {
            ---------省略----------
          //5、若以上緩存中都沒有找到對應的ViewHolder,最終會調用Adapter中的onCreateViewHolder創建一個
            holder = mAdapter.createViewHolder(RecyclerView.this, type);
        }
    }

    boolean bound = false;
    if (mState.isPreLayout() && holder.isBound()) {
        holder.mPreLayoutPosition = position;
    } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        //6、如果需要綁定數據,會調用Adapter#onBindViewHolder來綁定數據
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }
    ----------省略----------
    return holder;
}

上述邏輯用流程圖表示:

總結一下上述流程:通過mAttachedScrap、mCachedViews及mViewCacheExtension獲取的ViewHolder不需要重新創建佈局及綁定數據;通過緩存池mRecyclerPool獲取的ViewHolder不需要重新創建佈局,但是需要重新綁定數據;如果上述緩存中都沒有獲取到目標ViewHolder,那麼就會回調Adapter#onCreateViewHolder創建佈局,以及回調Adapter#onBindViewHolder來綁定數據。

ViewCacheExtension

我們已經知道ViewCacheExtension屬於第三級緩存,需要開發者自行實現,那麼ViewCacheExtension在什麼場景下使用?又是如何實現的呢?

首先我們要明確一點,那就是Recycler本身已經設置了好幾級緩存了,爲什麼還要留個接口讓開發者去自行實現緩存呢?關於這一點,談一談我的理解:來看看Recycler中的其他緩存,其中mAttachedScrap用來處理可見屏幕的緩存;mCachedViews裏存儲的數據雖然是根據position來緩存,但是裏面的數據隨時可能會被替換的;再來看mRecyclerPoolmRecyclerPool裏按viewType去存儲ArrayList< ViewHolder>,所以mRecyclerPool並不能按position去存儲ViewHolder,而且從mRecyclerPool取出的View每次都要去走Adapter#onBindViewHolder去重新綁定數據。假如我現在需要在一個特定的位置(比如position=0位置)一直展示某個View,且裏面的內容是不變的,那麼最好的情況就是在特定位置時,既不需要每次重新創建View,也不需要每次都去重新綁定數據,上面的幾種緩存顯然都是不適用的,這種情況該怎麼辦呢?可以通過自定義緩存ViewCacheExtension實現上述需求。

  • ViewCacheExtension適用場景:ViewHolder位置固定、內容固定、數量有限時使用
  • ViewCacheExtension使用舉例:
    比如在position=0時展示的是一個廣告,位置不變,內容不變,來看看如何實現:
    DemoRvActivity.java:
  public class DemoRvActivity extends AppCompatActivity {
    private RecyclerView recyclerView;
    private DemoAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_demo_rv);
        recyclerView = findViewById(R.id.rv_view);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
        adapter = new DemoAdapter();
        recyclerView.setAdapter(adapter);

        //viewType類型爲TYPE_SPECIAL時,設置四級緩存池RecyclerPool不存儲對應類型的數據 因爲需要開發者自行緩存
        recyclerView.getRecycledViewPool().setMaxRecycledViews(DemoAdapter.TYPE_SPECIAL, 0);
        //設置ViewCacheExtension緩存
        recyclerView.setViewCacheExtension(new MyViewCacheExtension());
    }

    //實現自定義緩存ViewCacheExtension
    class MyViewCacheExtension extends RecyclerView.ViewCacheExtension {
        @Nullable
        @Override
        public View getViewForPositionAndType(@NonNull RecyclerView.Recycler recycler, int position, int viewType) {
            //如果viewType爲TYPE_SPECIAL,使用自己緩存的View去構建ViewHolder
            // 否則返回null,會使用系統RecyclerPool緩存或者從新通過onCreateViewHolder構建View及ViewHolder
            return viewType == DemoAdapter.TYPE_SPECIAL ? adapter.caches.get(position) : null;
        }
    }
 }

在看下Adapter的代碼:

public class DemoAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    //viewType類型 TYPE_COMMON代表普通類型 TYPE_SPECIAL代表特殊類型(此處的View和數據一直不變)
    public static final int TYPE_COMMON = 1;
    public static final int TYPE_SPECIAL = 101;

    public SparseArray<View> caches = new SparseArray<>();//開發者自行維護的緩存

    private List<String> mDatas = new ArrayList<>();

    DemoAdapter() {
        initData();
    }

    private void initData() {
        for (int i = 0; i < 50; i++) {
            if (i == 0) {
                mDatas.add("我是一條特殊的數據,我的位置固定、內容不會變");
            } else {
                mDatas.add("這是第" + (i + 1) + "條數據");
            }
        }
    }

    public List<String> getData() {
        return mDatas;
    }

    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
        Log.e("TTT", "-----onCreateViewHolder:" + "viewType is " + viewType + "-----");
        Context context = viewGroup.getContext();
        if (viewType == TYPE_SPECIAL) {
            View view = LayoutInflater.from(context)
                    .inflate(R.layout.item_special_layout, viewGroup, false);
            return new SpecialHolder(view);
        } else {
            View view = LayoutInflater.from(context)
                    .inflate(R.layout.item_common_layout, viewGroup, false);
            return new CommonHolder(view);
        }
    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        Log.e("TTT", "-----onBindViewHolder:" + "position is " + position + "-----");
        if (holder instanceof SpecialHolder) {
            SpecialHolder sHolder = (SpecialHolder) holder;
            sHolder.tv_ad.setText(mDatas.get(position));
            //這裏是重點,根據position將View放到自定義緩存中
            caches.put(position, sHolder.itemView);
        } else if (holder instanceof CommonHolder) {
            CommonHolder cHolder = (CommonHolder) holder;
            cHolder.tv_textName.setText(mDatas.get(position));
        }
    }

    @Override
    public int getItemViewType(int position) {
        if (position == 0) {
            return TYPE_SPECIAL;//第一個位置View和數據固定
        } else {
            return TYPE_COMMON;
        }
    }

    @Override
    public long getItemId(int position) {
        return super.getItemId(position);
    }

    @Override
    public int getItemCount() {
        return mDatas.size();
    }

    class SpecialHolder extends RecyclerView.ViewHolder {
        TextView tv_ad;

        public SpecialHolder(@NonNull View itemView) {
            super(itemView);
            tv_ad = itemView.findViewById(R.id.tv_special_ad);
        }
    }

    class CommonHolder extends RecyclerView.ViewHolder {

        TextView tv_textName;

        public CommonHolder(@NonNull View itemView) {
            super(itemView);
            tv_textName = itemView.findViewById(R.id.tv_text);
        }
    }
}

運行界面如下:


重點關注第一條數據,當第一次運行時,在針對於第一條數據會執行Adapter#onCreateViewHolderAdapter#onBindViewHolder,想想也對,畢竟第一次執行,肯定要有一個創建View和綁定數據的過程。此時向下滑動到底部再滑上來,通過debug發現不再走這兩個方法了,而是在getViewForPositionAndType回調中根據position拿到了我們自定義緩存中的View及數據,所以可以直接展示。再看我們自己維護的緩存是什麼時候設置的,其實我這裏是在Adapter#onBindViewHolder中根據position設置的緩存:

caches.put(position, sHolder.itemView);

假如我們把上面這行代碼刪除了呢,再次執行上述滑動操作,自定義緩存對應失效了,Adapter#onCreateViewHolderAdapter#onBindViewHolder都會被執行,這裏可能大家可能會有個疑問,自定義緩存失效,爲什麼RecyclerPool裏也沒有對這個viewType進行緩存呢(因爲如果緩存了,是不會重新執行onCreateViewHolder的)?猜想這是因爲我在代碼中設置了

recyclerView.getRecycledViewPool().setMaxRecycledViews(DemoAdapter.TYPE_SPECIAL, 0)

viewType類型爲TYPE_SPECIAL時,設置緩存池RecyclerPool不存儲對應類型的數據,因爲開發者自行緩存了,所以沒必要再往RecyclerPool存儲了,如果把上面這行代碼註釋掉,重新執行上述滑動操作,會發現針對第一條數據只執行了Adapter#onBindViewHolder,因爲即使自定義緩存失效了,默認還是會往RecyclerPool存儲的嘛,這也驗證了我們的猜想。

RecyclerView & ListView緩存機制對比

結論援引自:Android ListView 與 RecyclerView 對比淺析--緩存機制

ListView和RecyclerView緩存機制基本一致:
1). mActiveViews和mAttachedScrap功能相似,意義在於快速重用屏幕上可見的列表項ItemView,而不需要重新createView和bindView;
2). mScrapView和mCachedViews + mReyclerViewPool功能相似,意義在於緩存離開屏幕的ItemView,目的是讓即將進入屏幕的ItemView重用.
3). RecyclerView的優勢在於a.mCacheViews的使用,可以做到屏幕外的列表項ItemView進入屏幕內時也無須bindView快速重用;b.mRecyclerPool可以供多個RecyclerView共同使用,在特定場景下,如viewpaper+多個列表頁下有優勢.客觀來說,RecyclerView在特定場景下對ListView的緩存機制做了補強和完善。
不同使用場景:列表頁展示界面,需要支持動畫,或者頻繁更新,局部刷新,建議使用RecyclerView,更加強大完善,易擴展;其它情況(如微信卡包列表頁)兩者都OK,但ListView在使用上會更加方便,快捷。

參考

【1】關於Recyclerview的緩存機制的理解
【2】RecyclerView緩存機制
【3】ViewCacheExtension使用
【4】RecyclerView 必知必會

【5】https://juejin.im/post/5a7569676fb9a063435eaf4c
【6】https://github.com/gyzboy/AndroidSamples/blob/master/app/src/main/java/com/gyz/androidsamples/view/ASRecyclerView.java
【7】https://blog.csdn.net/HJsir/article/details/81485653
【8】RecyclerView緩存原理,有圖有真相

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章