RecyclerView 的緩存剖析

先從 getViewByPosition() 開(kāi)始,LayoutManager 會(huì)詢問(wèn) RecyclerView,請(qǐng)?jiān)?position 為8的位置給我一個(gè)View。 這是RecycleView所做的響應(yīng):

  1. 搜索 changed scrap
  2. 搜索 attached scrap(屏幕內(nèi))
  3. 搜索 未刪除的隱藏視圖
  4. 搜索 view cache(屏幕外)
  5. 如果適配器具有穩(wěn)定的 ID,用 ID 再次去搜索 attached scrap 和 view cache。
  6. 搜索 ViewCacheExtension
  7. 搜索 RecycledViewPool

如果在所有這些地方都找不到合適的 View,則會(huì)通過(guò)調(diào)用適配器的onCreateViewHolder()方法來(lái)創(chuàng)建一個(gè) View 。 然后,如有必要它通過(guò) onBindViewHolder()綁定 View,最后返回它。

        /**
         * Called when RecyclerView needs a new {@link ViewHolder} of the given type to represent
         * an item.
         * <p>
         * This new ViewHolder should be constructed with a new View that can represent the items
         * of the given type. You can either create a new View manually or inflate it from an XML
         * layout file.
         * <p>
         * The new ViewHolder will be used to display items of the adapter using
         * {@link #onBindViewHolder(ViewHolder, int, List)}. Since it will be re-used to display
         * different items in the data set, it is a good idea to cache references to sub views of
         * the View to avoid unnecessary {@link View#findViewById(int)} calls.
         *
         * @param parent The ViewGroup into which the new View will be added after it is bound to
         *               an adapter position.
         * @param viewType The view type of the new View.
         *
         * @return A new ViewHolder that holds a View of the given view type.
         * @see #getItemViewType(int)
         * @see #onBindViewHolder(ViewHolder, int)
         */
        @NonNull
        public abstract VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType);

如你所見(jiàn),這里發(fā)生了很多事情,我們的目標(biāo)是弄清楚所有這些緩存的含義,它們?nèi)绾喂ぷ饕约盀槭裁葱枰鼈?,我們將逐一介紹它們 。

通常認(rèn)為 RecyclerView 有四級(jí)緩存,RecyclerView 的緩存是通過(guò) Recycler 類(lèi)來(lái)完成的,方法的入口:

        /**
         * Obtain a view initialized for the given position.
         *
         * This method should be used by {@link LayoutManager} implementations to obtain
         * views to represent data from an {@link Adapter}.
         * <p>
         * The Recycler may reuse a scrap or detached view from a shared pool if one is
         * available for the correct view type. If the adapter has not indicated that the
         * data at the given position has changed, the Recycler will attempt to hand back
         * a scrap view that was previously initialized for that data without rebinding.
         *
         * @param position Position to obtain a view for
         * @return A view representing the data at <code>position</code> from <code>adapter</code>
         */
        @NonNull
        public View getViewForPosition(int position) {
            return getViewForPosition(position, false);
        }

緩存的內(nèi)容是 ViewHolder,緩存的地方,是 Recycler 的幾個(gè) list:

    /**
     * A Recycler is responsible for managing scrapped or detached item views for reuse.
     *
     * <p>A "scrapped" view is a view that is still attached to its parent RecyclerView but
     * that has been marked for removal or reuse.</p>
     *
     * <p>Typical use of a Recycler by a {@link LayoutManager} will be to obtain views for
     * an adapter's data set representing the data at a given position or item ID.
     * If the view to be reused is considered "dirty" the adapter will be asked to rebind it.
     * If not, the view can be quickly reused by the LayoutManager with no further work.
     * Clean views that have not {@link android.view.View#isLayoutRequested() requested layout}
     * may be repositioned by a LayoutManager without remeasurement.</p>
     */
    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;
 ...省略
}
第一級(jí)緩存

mAttachedScrap: 用于緩存顯示在屏幕上的 item 的 ViewHolder?!皊crapped”視圖是仍附加到其父 RecyclerView 的視圖,但已標(biāo)記為可刪除或重復(fù)使用。用于緩存顯示在屏幕上的 item 的 ViewHolder??梢钥吹竭@個(gè)變量是個(gè)存放 ViewHolder 對(duì)象的ArrayList ,而且是沒(méi)有容量限制的,它是屬于 Scrap 的一種,這里的數(shù)據(jù)是不做修改的,不會(huì)重新走Adapter的綁定方法的。

mChangedScrap: 跟 ViewHolder 的數(shù)據(jù)發(fā)生變化時(shí)有關(guān)吧。這個(gè)變量和 mAttachedScrap 是一樣的,唯一不同的是,它存放的是發(fā)生變化的 ViewHolder ,如果使用到這里緩存的 ViewHolder 是要重新走 Adapter 的綁定方法的。

第二級(jí)緩存

mCachedViews:劃出屏幕外的 item,這個(gè) list 的默認(rèn)大小是2。這個(gè)就重要得多了,滑動(dòng)過(guò)程中的回收和復(fù)用都是先處理的這個(gè) List,這個(gè)集合里存的 ViewHolder 的原本數(shù)據(jù)信息都在,所以可以直接添加到 RecyclerView 中顯示,不需要再次重新 onBindViewHolder()。這個(gè)變量同樣是一個(gè)存放 ViewHolder 對(duì)象的 ArrayList ,但是這個(gè)不同于上面的兩個(gè)里面存放的是顯示在屏幕上的視圖,它里面存放的是已經(jīng) remove 掉的視圖,已經(jīng)和 RecyclerView 分離關(guān)系的視圖,但是它里面的 ViewHolder 依然保存著之前的信息(綁定的數(shù)據(jù)以及位置信息等),而且它的容量是有限的默認(rèn)是2(不同的API可能會(huì)有差異),同樣它的大小也是可以修改的,合理的改變它的大小,可以減少 ViewHolder 數(shù)據(jù)綁定的次數(shù)。

第三級(jí)緩存

mViewCacheExtension:自定義緩存,RecyclerView 默認(rèn)是沒(méi)有實(shí)現(xiàn)的, ViewCacheExtension 是一個(gè)幫助程序類(lèi),用于提供附加的視圖緩存層,該緩存可以由開(kāi)發(fā)者控制。

第四級(jí)緩存

mRecyclerPool:這個(gè)也很重要,但存在這里的 ViewHolder 的數(shù)據(jù)信息會(huì)被重置掉,相當(dāng)于 ViewHolder 是一個(gè)重新新建的一樣,所以需要重新調(diào)用 onBindViewHolder 來(lái)綁定數(shù)據(jù)。這個(gè)變量是一個(gè)類(lèi)和上面三個(gè)不一樣,這里面保存的 ViewHolder 不僅僅是 remove 掉的視圖,而且是“恢復(fù)出廠設(shè)置”的視圖,任何綁定過(guò)的痕跡都沒(méi)有了,如果想用這里的緩存的 ViewHolder 那就要重新走 Adapter 的綁定方法,所以盡量不要讓 ViewHolder 進(jìn)入這一層。因?yàn)?RecyclerView 是支持多布局的,所以 mRecyclerPool 的緩存是按照 itemType 來(lái)分開(kāi)存儲(chǔ)的,來(lái)看一下它的結(jié)構(gòu):

  • 首先我們看到一個(gè)常量‘DEFAULT_MAX_SCRAP’默認(rèn)值為5,這個(gè)就是一個(gè)緩存池的默認(rèn)緩存數(shù)。它不是整個(gè)緩存池的總數(shù),它是每個(gè)對(duì)應(yīng) itemType 類(lèi)型的默認(rèn)緩存數(shù),當(dāng)然你可以針對(duì)不同的類(lèi)型修改其緩存數(shù)的大小,適當(dāng)?shù)男薷木彺鏀?shù)的大小可以減少 ViewHolder 的創(chuàng)建數(shù)量。你可以像這樣更改它:
recyclerView.getRecycledViewPool()
            .setMaxRecycledViews(SOME_VIEW_TYPE, POOL_CAPACITY);

這是非常重要的靈活性。如果屏幕上有數(shù)十個(gè)相同類(lèi)型的項(xiàng)目,這些項(xiàng)目經(jīng)常同時(shí)更改,請(qǐng)為該視圖類(lèi)型增大池。并且,如果您知道某些視圖類(lèi)型的項(xiàng)目非常稀有,以至于它們?cè)谄聊簧巷@示的數(shù)量永遠(yuǎn)不會(huì)超過(guò)一個(gè),請(qǐng)為該視圖類(lèi)型設(shè)置池大小1。否則,遲早池中將充滿其中的5個(gè)項(xiàng)目,而其中4個(gè)項(xiàng)目只會(huì)閑置在那兒,這會(huì)浪費(fèi)內(nèi)存。
getRecyclerView()、putRecycledView()、clear()方法是公共的,因此你可以操縱池的內(nèi)容。手動(dòng)使用 putRecycledView(),例如事先準(zhǔn)備一些 ViewHolders,不過(guò)這不是一個(gè)好想法。你只能在適配器的 onCreateViewHolder()方法中創(chuàng)建 ViewHolder,否則 ViewHolders 可能會(huì)以 RecyclerView 所不希望的狀態(tài)出現(xiàn)。另一個(gè)很酷的功能是,與 getRecycledViewPool()一起有一個(gè) setRecycledViewPool(),因此你可以將單個(gè)池重用于多個(gè)RecycleViews。最后,我會(huì)注意到每種視圖類(lèi)型的池都是堆棧(后進(jìn)先出)。。

  • 我們看到一個(gè)靜態(tài)內(nèi)部類(lèi) ScrapData ,我們還看到了 mMaxScrap 并且前面的常量賦值給了它,這就解釋了上面提到的,這個(gè)緩存數(shù)量是對(duì)應(yīng)不同 itemType 類(lèi)型的緩存數(shù),再看一下 mScrapHeap 同樣是一個(gè)緩存 ViewHolder 的 ArrayList ,這就說(shuō)明ScrapData 類(lèi)是 mScrapHeap 對(duì) ViewHolder 進(jìn)行緩存,并且數(shù)組的最大值為5的類(lèi)的一個(gè)封裝。
  • 最后我們看到了 mScrap 這個(gè)變量,它是一個(gè)存儲(chǔ)我們上面提到的 ScrapData 類(lèi)的對(duì)象的 SparseArray,這樣就解釋了 RecyclerPool 是不同 itemType 的 ViewHolder 按 itemType 類(lèi)型分類(lèi)緩存起來(lái)的。

mCachedViews 的數(shù)量達(dá)到上限之后,會(huì)把 ViewHolder 存入 mRecyclerPool。mRecyclerPool 用 SparseArray 來(lái)緩存進(jìn)入這一級(jí)的 ViewHolder:

    /**
     * RecycledViewPool lets you share Views between multiple RecyclerViews.
     * <p>
     * If you want to recycle views across RecyclerViews, create an instance of RecycledViewPool
     * and use {@link RecyclerView#setRecycledViewPool(RecycledViewPool)}.
     * <p>
     * RecyclerView automatically creates a pool for itself if you don't provide one.
     */
    public static class RecycledViewPool {
        private static final int DEFAULT_MAX_SCRAP = 5;

        /**
         * Tracks both pooled holders, as well as create/bind timing metadata for the given type.
         *
         * Note that this tracks running averages of create/bind time across all RecyclerViews
         * (and, indirectly, Adapters) that use this pool.
         *
         * 1) This enables us to track average create and bind times across multiple adapters. Even
         * though create (and especially bind) may behave differently for different Adapter
         * subclasses, sharing the pool is a strong signal that they'll perform similarly, per type.
         *
         * 2) If {@link #willBindInTime(int, long, long)} returns false for one view, it will return
         * false for all other views of its type for the same deadline. This prevents items
         * constructed by {@link GapWorker} prefetch from being bound to a lower priority prefetch.
         */
        static class ScrapData {
            final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
            int mMaxScrap = DEFAULT_MAX_SCRAP;
            long mCreateRunningAverageNs = 0;
            long mBindRunningAverageNs = 0;
        }
        SparseArray<ScrapData> mScrap = new SparseArray<>();

        private int mAttachCount = 0;
...省略
}

現(xiàn)在,讓我們解決將 ViewHolders 扔入池中的時(shí)機(jī)問(wèn)題。 有5種情況:

  1. 在滾動(dòng)過(guò)程中,視圖超出了 RecyclerView 的范圍。
  2. 數(shù)據(jù)已更改,因此視圖不再可見(jiàn)。 消失動(dòng)畫(huà)結(jié)束時(shí),會(huì)添加到池中。
  3. 視圖緩存中的項(xiàng)目已更新或刪除。
  4. 在搜索 ViewHolder 時(shí),在 scrap 或 mCachedViews 中找到了我們想要的位置,但由于視圖類(lèi)型或 ID 錯(cuò)誤(如果適配器具有穩(wěn)定的 ID ),結(jié)果證明不合適。
  5. LayoutManager 在布局前添加了一個(gè)視圖,但未在布局后添加該視圖。

前兩種情況非常明顯。 但是,要注意的一件事是,第2種情況不僅通過(guò)刪除有問(wèn)題的項(xiàng)目來(lái)觸發(fā),而且還可以通過(guò)例如插入其他項(xiàng)目來(lái)觸發(fā),從而將給定項(xiàng)目推出了界限。

最后說(shuō)下:緩存優(yōu)化

第一種優(yōu)化方法:
進(jìn)入 RecyclerPool 的 ViewHolder 會(huì)被重置,會(huì)從新執(zhí)行 bindViewHolder,所以從效率上來(lái)講,很費(fèi)性能。所以為了避免進(jìn)入這一層緩存,可以在在第三層自定義緩存自己實(shí)現(xiàn),也就是自定義 mViewCacheExtension 。在這里自己維護(hù)一個(gè) viewType 對(duì)應(yīng) View 的 SparseArray 。這樣可以避免因?yàn)槎喾N type 導(dǎo)致的 holder 重建。

    /**
     * ViewCacheExtension is a helper class to provide an additional layer of view caching that can
     * be controlled by the developer.
     * <p>
     * When {@link Recycler#getViewForPosition(int)} is called, Recycler checks attached scrap and
     * first level cache to find a matching View. If it cannot find a suitable View, Recycler will
     * call the {@link #getViewForPositionAndType(Recycler, int, int)} before checking
     * {@link RecycledViewPool}.
     * <p>
     * Note that, Recycler never sends Views to this method to be cached. It is developers
     * responsibility to decide whether they want to keep their Views in this custom cache or let
     * the default recycling policy handle it.
     */
    public abstract static class ViewCacheExtension {

        /**
         * Returns a View that can be binded to the given Adapter position.
         * <p>
         * This method should <b>not</b> create a new View. Instead, it is expected to return
         * an already created View that can be re-used for the given type and position.
         * If the View is marked as ignored, it should first call
         * {@link LayoutManager#stopIgnoringView(View)} before returning the View.
         * <p>
         * RecyclerView will re-bind the returned View to the position if necessary.
         *
         * @param recycler The Recycler that can be used to bind the View
         * @param position The adapter position
         * @param type     The type of the View, defined by adapter
         * @return A View that is bound to the given position or NULL if there is no View to re-use
         * @see LayoutManager#ignoreView(View)
         */
        @Nullable
        public abstract View getViewForPositionAndType(@NonNull Recycler recycler, int position,
                int type);
    }

注意 getViewForPositionAndType 返回的是 view 而不是 ViewHolder,然后會(huì)通過(guò)view 的 layoutParams 拿到 ViewHolder。
例如可以這么寫(xiě):

SparseArray<View> specials = new SparseArray<>();
...

recyclerView.getRecycledViewPool().setMaxRecycledViews(SPECIAL, 0);

recyclerView.setViewCacheExtension(new RecyclerView.ViewCacheExtension() {
   @Override
   public View getViewForPositionAndType(RecyclerView.Recycler recycler,
                                         int position, int type) {
       return type == SPECIAL ? specials.get(position) : null;
   }
});

...
class SpecialViewHolder extends RecyclerView.ViewHolder {
       ...      
   public void bindTo(int position) {
       ...
       specials.put(position, itemView);
   }
}

第二種優(yōu)化方法:
可以增大 mCachedViews 的緩存數(shù)量,改成你需要的量。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容