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 来进行局部刷新数据,而不是一味地全局刷新数据。

  • 减少布局层级过渡绘制
    可以通过自定义 View 或者更合理地设置布局来减少层级,移除不必要的背景减少过度绘制。

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