美文网首页Android 开发进阶
RecyclerView 的缓存剖析

RecyclerView 的缓存剖析

作者: JeffreyWorld | 来源:发表于2020-09-29 18:03 被阅读0次

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

相关文章

网友评论

    本文标题:RecyclerView 的缓存剖析

    本文链接:https://www.haomeiwen.com/subject/myueuktx.html