RecyclerView 的缓存剖析

先从 getViewByPosition() 开始,LayoutManager 会询问 RecyclerView,请在 position 为8的位置给我一个View。 这是RecycleView所做的响应:

  1. 搜索 changed scrap
  2. 搜索 attached scrap(屏幕内)
  3. 搜索 未删除的隐藏视图
  4. 搜索 view cache(屏幕外)
  5. 如果适配器具有稳定的 ID,用 ID 再次去搜索 attached scrap 和 view cache。
  6. 搜索 ViewCacheExtension
  7. 搜索 RecycledViewPool

如果在所有这些地方都找不到合适的 View,则会通过调用适配器的onCreateViewHolder()方法来创建一个 View 。 然后,如有必要它通过 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);

如你所见,这里发生了很多事情,我们的目标是弄清楚所有这些缓存的含义,它们如何工作以及为什么需要它们,我们将逐一介绍它们 。

通常认为 RecyclerView 有四级缓存,RecyclerView 的缓存是通过 Recycler 类来完成的,方法的入口:

        /**
         * 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);
        }

缓存的内容是 ViewHolder,缓存的地方,是 Recycler 的几个 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;
 ...省略
}
第一级缓存

mAttachedScrap: 用于缓存显示在屏幕上的 item 的 ViewHolder。“scrapped”视图是仍附加到其父 RecyclerView 的视图,但已标记为可删除或重复使用。用于缓存显示在屏幕上的 item 的 ViewHolder。可以看到这个变量是个存放 ViewHolder 对象的ArrayList ,而且是没有容量限制的,它是属于 Scrap 的一种,这里的数据是不做修改的,不会重新走Adapter的绑定方法的。

mChangedScrap: 跟 ViewHolder 的数据发生变化时有关吧。这个变量和 mAttachedScrap 是一样的,唯一不同的是,它存放的是发生变化的 ViewHolder ,如果使用到这里缓存的 ViewHolder 是要重新走 Adapter 的绑定方法的。

第二级缓存

mCachedViews:划出屏幕外的 item,这个 list 的默认大小是2。这个就重要得多了,滑动过程中的回收和复用都是先处理的这个 List,这个集合里存的 ViewHolder 的原本数据信息都在,所以可以直接添加到 RecyclerView 中显示,不需要再次重新 onBindViewHolder()。这个变量同样是一个存放 ViewHolder 对象的 ArrayList ,但是这个不同于上面的两个里面存放的是显示在屏幕上的视图,它里面存放的是已经 remove 掉的视图,已经和 RecyclerView 分离关系的视图,但是它里面的 ViewHolder 依然保存着之前的信息(绑定的数据以及位置信息等),而且它的容量是有限的默认是2(不同的API可能会有差异),同样它的大小也是可以修改的,合理的改变它的大小,可以减少 ViewHolder 数据绑定的次数。

第三级缓存

mViewCacheExtension:自定义缓存,RecyclerView 默认是没有实现的, ViewCacheExtension 是一个帮助程序类,用于提供附加的视图缓存层,该缓存可以由开发者控制。

第四级缓存

mRecyclerPool:这个也很重要,但存在这里的 ViewHolder 的数据信息会被重置掉,相当于 ViewHolder 是一个重新新建的一样,所以需要重新调用 onBindViewHolder 来绑定数据。这个变量是一个类和上面三个不一样,这里面保存的 ViewHolder 不仅仅是 remove 掉的视图,而且是“恢复出厂设置”的视图,任何绑定过的痕迹都没有了,如果想用这里的缓存的 ViewHolder 那就要重新走 Adapter 的绑定方法,所以尽量不要让 ViewHolder 进入这一层。因为 RecyclerView 是支持多布局的,所以 mRecyclerPool 的缓存是按照 itemType 来分开存储的,来看一下它的结构:

  • 首先我们看到一个常量‘DEFAULT_MAX_SCRAP’默认值为5,这个就是一个缓存池的默认缓存数。它不是整个缓存池的总数,它是每个对应 itemType 类型的默认缓存数,当然你可以针对不同的类型修改其缓存数的大小,适当的修改缓存数的大小可以减少 ViewHolder 的创建数量。你可以像这样更改它:
recyclerView.getRecycledViewPool()
            .setMaxRecycledViews(SOME_VIEW_TYPE, POOL_CAPACITY);

这是非常重要的灵活性。如果屏幕上有数十个相同类型的项目,这些项目经常同时更改,请为该视图类型增大池。并且,如果您知道某些视图类型的项目非常稀有,以至于它们在屏幕上显示的数量永远不会超过一个,请为该视图类型设置池大小1。否则,迟早池中将充满其中的5个项目,而其中4个项目只会闲置在那儿,这会浪费内存。
getRecyclerView()、putRecycledView()、clear()方法是公共的,因此你可以操纵池的内容。手动使用 putRecycledView(),例如事先准备一些 ViewHolders,不过这不是一个好想法。你只能在适配器的 onCreateViewHolder()方法中创建 ViewHolder,否则 ViewHolders 可能会以 RecyclerView 所不希望的状态出现。另一个很酷的功能是,与 getRecycledViewPool()一起有一个 setRecycledViewPool(),因此你可以将单个池重用于多个RecycleViews。最后,我会注意到每种视图类型的池都是堆栈(后进先出)。。

  • 我们看到一个静态内部类 ScrapData ,我们还看到了 mMaxScrap 并且前面的常量赋值给了它,这就解释了上面提到的,这个缓存数量是对应不同 itemType 类型的缓存数,再看一下 mScrapHeap 同样是一个缓存 ViewHolder 的 ArrayList ,这就说明ScrapData 类是 mScrapHeap 对 ViewHolder 进行缓存,并且数组的最大值为5的类的一个封装。
  • 最后我们看到了 mScrap 这个变量,它是一个存储我们上面提到的 ScrapData 类的对象的 SparseArray,这样就解释了 RecyclerPool 是不同 itemType 的 ViewHolder 按 itemType 类型分类缓存起来的。

mCachedViews 的数量达到上限之后,会把 ViewHolder 存入 mRecyclerPool。mRecyclerPool 用 SparseArray 来缓存进入这一级的 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;
...省略
}

现在,让我们解决将 ViewHolders 扔入池中的时机问题。 有5种情况:

  1. 在滚动过程中,视图超出了 RecyclerView 的范围。
  2. 数据已更改,因此视图不再可见。 消失动画结束时,会添加到池中。
  3. 视图缓存中的项目已更新或删除。
  4. 在搜索 ViewHolder 时,在 scrap 或 mCachedViews 中找到了我们想要的位置,但由于视图类型或 ID 错误(如果适配器具有稳定的 ID ),结果证明不合适。
  5. LayoutManager 在布局前添加了一个视图,但未在布局后添加该视图。

前两种情况非常明显。 但是,要注意的一件事是,第2种情况不仅通过删除有问题的项目来触发,而且还可以通过例如插入其他项目来触发,从而将给定项目推出了界限。

最后说下:缓存优化

第一种优化方法:
进入 RecyclerPool 的 ViewHolder 会被重置,会从新执行 bindViewHolder,所以从效率上来讲,很费性能。所以为了避免进入这一层缓存,可以在在第三层自定义缓存自己实现,也就是自定义 mViewCacheExtension 。在这里自己维护一个 viewType 对应 View 的 SparseArray 。这样可以避免因为多种 type 导致的 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,然后会通过view 的 layoutParams 拿到 ViewHolder。
例如可以这么写:

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);
   }
}

第二种优化方法:
可以增大 mCachedViews 的缓存数量,改成你需要的量。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343