先从 getViewByPosition() 开始,LayoutManager 会询问 RecyclerView,请在 position 为8的位置给我一个View。 这是RecycleView所做的响应:
- 搜索 changed scrap
- 搜索 attached scrap(屏幕内)
- 搜索 未删除的隐藏视图
- 搜索 view cache(屏幕外)
- 如果适配器具有稳定的 ID,用 ID 再次去搜索 attached scrap 和 view cache。
- 搜索 ViewCacheExtension
- 搜索 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种情况:
- 在滚动过程中,视图超出了 RecyclerView 的范围。
- 数据已更改,因此视图不再可见。 消失动画结束时,会添加到池中。
- 视图缓存中的项目已更新或删除。
- 在搜索 ViewHolder 时,在 scrap 或 mCachedViews 中找到了我们想要的位置,但由于视图类型或 ID 错误(如果适配器具有稳定的 ID ),结果证明不合适。
- 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 的缓存数量,改成你需要的量。