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.
         * 

* 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. *

* 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}.
         * 

* 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 position from adapter */ @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.
     *
     * 

A "scrapped" view is a view that is still attached to its parent RecyclerView but * that has been marked for removal or reuse.

* *

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.

*/ public final class Recycler { final ArrayList mAttachedScrap = new ArrayList<>(); ArrayList mChangedScrap = null; final ArrayList mCachedViews = new ArrayList(); private final List 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.
     * 

* If you want to recycle views across RecyclerViews, create an instance of RecycledViewPool * and use {@link RecyclerView#setRecycledViewPool(RecycledViewPool)}. *

* 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 mScrapHeap = new ArrayList<>(); int mMaxScrap = DEFAULT_MAX_SCRAP; long mCreateRunningAverageNs = 0; long mBindRunningAverageNs = 0; } SparseArray 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.
     * 

* 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}. *

* 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. *

* This method should not 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. *

* 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 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 的缓存数量,改成你需要的量。

你可能感兴趣的:(RecyclerView 的缓存剖析)