RecyclerView 简析之缓存机制及优化
RecyclerView 是用于大量数据展示的控件,相对于传统的 ListView ,更加强大和灵活。
缓存机制
RecyclerView 与 ListView 的缓存机制原理大致相似, 滑动的时候,离屏的 ItemView 被回收至缓存,入屏的 ItemView 则会优先从缓存中获取,只是 ListView 与 RecyclerView 的实现细节有差异。
ListView 缓存机制
ListView 主要是二级缓存,缓存的对象是 View,ListView 是继承于 AbsListView 的,而 AbsListView 里面有个 mRecycler,用于存储不使用的 view,其将被下次 layout 的时候重新使用,以避免创建新的实例。
/**
* The data set used to store unused views that should be reused during the next layout
* to avoid creating new ones
*/
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123769398)
final RecycleBin mRecycler = new RecycleBin();
RecycleBin 是 AbsListView 的内部类,其作用是通过两级缓存来缓存 view。(RecycleBin 在 layout 的过程中便于 view 重用,RecycleBin 有两级缓存:mActiveViews 和 mScrapViews)。
- mActiveViews
第一级缓存,这些 View 是布局过程开始时屏幕上的 view,layout 开始时这个数组被填充,layout 结束,mActiveViews 中的 View 移动到 mScrapView,意义在于快速重用屏幕上可见的列表项 ItemView,而不需要重新 createView 和 bindView。 - mScrapView
第二级缓存,mScrapView 是多个 List 组成的数据,数组的长度为 viewTypeCount,每个 List 缓存不同类型 Item 布局的 View,其意义在于缓存离开屏幕的 ItemView,目的是让即将进入屏幕的 itemView 重用,当 mAdapter 被更换时,mScrapViews 则被清空。
RecyclerView 缓存机制
同样地,RecyclerView 也有一个类专门来管理缓存,不过与 ListView 不同的是,RecylerView 缓存的是 ViewHolder,而且实现的是四级缓存,如下:
public final class Recycler {
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
private ArrayList<ViewHolder> mChangedScrap = null;
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
private final List<ViewHolder>
mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);
private RecycledViewPool mRecyclerPool;
private ViewCacheExtension mViewCacheExtension;
- mAttachedScrap
第一级缓存,相当于 ListView 的 mActiveView,快速重用屏幕上可见的 ViewHolder。 - mCacheViews
第二级缓存,如果仍依赖于 RecyclerView(比如已经滑出可视范围,但还没有被移除掉),但已经被标记移除的 ItemView 集合被添加到 mAttachedScrap 中。然后如果 mAttachedScrap 中不再依赖时会被加入到 mCachedViews 中,默认缓存 2 个 ItemView,RecycleView 从这里获取的缓存时,如果数据源不变的情况下,无需重新 bindView。 - mViewCacheExtension
第三级缓存,其是一个抽象静态类,用于充当附加的缓存池,当 RecyclerView 从 mCacheViews 找不到需要的 View 时,将会从 ViewCacheExtension 中寻找。不过这个缓存是由开发者维护的,如果没有设置它,则不会启用。通常我们也不会设置它,除非有特殊需求,比如要在调用系统的缓存池之前,返回一个特定的视图,才会用到它。 - RecycledViewPool
第四级缓存,最强大的缓存器,代码如下:
public static class RecycledViewPool {
// 根据 viewType 保存的被废弃的 ViewHolder 集合,以便下次使用
private SparseArray<ArrayList<ViewHolder>> mScrap = new SparseArray<ArrayList<ViewHolder>>();
/**
* 从缓存池移除并返回一个 ViewHolder
*/
public ViewHolder getRecycledView(int viewType) {
final ArrayList<ViewHolder> scrapHeap = mScrap.get(viewType);
if (scrapHeap != null && !scrapHeap.isEmpty()) {
final int index = scrapHeap.size() - 1;
final ViewHolder scrap = scrapHeap.get(index);
scrapHeap.remove(index);
return scrap;
}
return null;
}
public void putRecycledView(ViewHolder scrap) {
final int viewType = scrap.getItemViewType();
final ArrayList scrapHeap = getScrapHeapForType(viewType);
if (mMaxScrap.get(viewType) <= scrapHeap.size()) {
return;
}
scrap.resetInternal();
scrapHeap.add(scrap);
}
/**
* 根据 viewType 获取对应缓存池
*/
private ArrayList<ViewHolder> getScrapHeapForType(int viewType) {
ArrayList<ViewHolder> scrap = mScrap.get(viewType);
if (scrap == null) {
scrap = new ArrayList<>();
mScrap.put(viewType, scrap);
if (mMaxScrap.indexOfKey(viewType) < 0) {
mMaxScrap.put(viewType, DEFAULT_MAX_SCRAP);
}
}
return scrap;
}
}
顾名思义,它是一个缓存池,实现上,是通过一个默认为 5 大小的 ArrayList 实现的。这一点,同 ListView 的 RecyclerBin 这个类一样。每一个 ArrayList 又都是放在一个 Map 里面的,SparseArray 用两个数组用来替代 Map。
把所有的 ArrayList 放在一个 Map 里面,这也是 RecyclerView 最大的亮点,这样根据 itemType 来取不同的缓存 Holder,每一个 Holder 都有对应的缓存,而只需要为这些不同 RecyclerView 设置同一个 Pool 就可以了。
这个可以在 Pool 的 setRecycledViewPool() 方法可以看到注释:
/**
* Recycled view pools allow multiple RecyclerViews to share a common pool of scrap views.
* This can be useful if you have multiple RecyclerViews with adapters that use the same
* view types, for example if you have several data sets with the same kinds of item views
* displayed by a {@link android.support.v4.view.ViewPager ViewPager}.
*
* @param pool Pool to set. If this parameter is null a new pool will be created and used.
*/
public void setRecycledViewPool(RecycledViewPool pool) {
mRecycler.setRecycledViewPool(pool);
}
RecyclerView 优化
- 数据处理和视频加载分离
耗时的数据处理逻辑应该放在异步处理,这样 Adapter 在 notify 改变数据时,ViewHolder 可以操作数据于视图的绑定逻辑。比如:
mTextView.setText(Html.fromHtml(data).toString());
这里的 Html.fromHtml(data) 方法可能就是比较耗时的,存在多个 TextView 的话耗时会更为严重,这样便会引发掉帧、卡顿,故此时应该在子线程处理。
-
数据优化
分页拉取数据时,对拉取下来的远端数据进行缓存,提升二次加载速度;对于新增或者删除数据通过 DiffUtil 来进行局部刷新数据,而不是一味地全局刷新数据。 -
减少 xml 文件 inflate 时间
这里的 xml 文件不仅包括 layout 的 xml,还包括 drawable 的 xml,xml 文件 inflate 出 ItemView 时通过耗时的 IO 操作,尤其当 Item 的复用几率很低的情况下,对着 Type 的增多,这种 inflate 带来的损耗时相当大的,此时我们可以用代码去生成布局。 -
减少 View 对象的创建
一个稍微复杂的 Item 会包含大量的 View,而大量的 View 的创建也会消耗大量时间,所以要尽可能简化 ItemView;设计 ItemType 时,对多 ViewType 能够共用的部分尽量设计成自定义 View,减少 View 的构造和嵌套。 -
使用 RecyclerView 的 prefetch 功能
-
如果 Item 高度是固定的话,可以使用 RecylerView.setHasFixedSize(true),来避免 requestLayout 浪费资源。
-
滑动过程冲停止数据加载或者图片加载工作。
-
如果不需要动画,把默认动画关闭来提升效率,动画在 Android 系统中是一个很大的开销。
-
通过 RecyclerView.setItemViewCacheSize(size);来加大 RecyclerView 的缓存,用空间换时间来提高滚动的流畅性。
-
如果多个 RecyclerView 的 Adapter 是一样的,比如嵌套的 RecyclerView 存在一样的 Adapter,可以通过设置 RecyclerView.setRecycledViewPool() 方法来共用一个 RecyledViewPool。
-
通过 getExtraLayoutSpace() 方法来增加 RecyclerView 预留的额外空间(显示范围之外,应额外缓存空间),如下:
new LinearLayoutManager(this) {
@Override
protected int getExtraLayoutSpace(RecyclerView.State state) {
return size;
}
};
- 在 onBindView 的时候只做数据绑定数据工作,不要创建对象。