RecyclerView缓存原理,有图有真相

20,090 阅读6分钟

1. RecyclerView缓存机制与性能优化关系

RecyclerView做性能优化要说复杂也复杂,比如说布局优化,缓存,预加载等等。其优化的点很多,在这些看似独立的点之间,其实存在一个枢纽:Adapter。因为所有的ViewHolder的创建和内容的绑定都需要经过Adaper的两个函数onCreateViewHolder和onBindViewHolder

因此我们性能优化的本质就是要减少这两个函数的调用时间和调用的次数。如果我们想对RecyclerView做性能优化,必须清楚的了解到我们的每一步操作背后,onCreateViewHolder和onBindViewHolder调用了多少次。因此,了解RecyclerView的缓存机制是RecyclerView性能优化的基础。

为了理解缓存的应用场景,本文首先会简单介绍一下RecyclerView的绘制原理,然后再分析其缓存实现原理。

这里写图片描述

2. 绘制原理简述

2.1 假设

为了简化问题,绘制原理介绍提供以下假设:

  • RecyclerView
    • 以LinearLayoutManager为例
    • 忽略ItemDecoration
    • 忽略ItemAnimator
    • 忽略Measure过程
    • 假设RecyclerView的width和height是确定的
  • Recycler
    • 忽略mViewCacheExtension

2.2 绘制过程

(1)类的职责介绍

LayoutManager:接管RecyclerView的Measure,Layout,Draw的过程

Recycler:缓存池

Adapter:ViewHolder的生成器和内容绑定器。

(2)绘制过程简介

  1. RecyclerView.requestLayout开始发生绘制,忽略Measure的过程
  2. 在Layout的过程会通过LayoutManager.fill去将RecyclerView填满
  3. LayoutManager.fill会调用LayoutManager.layoutChunk去生成一个具体的ViewHolder
  4. 然后LayoutManager就会调用Recycler.getViewForPosition向Recycler去要ViewHolder
  5. Recycler首先去一级缓存(Cache)里面查找是否命中,如果命中直接返回。如果一级缓存没有找到,则去三级缓存查找,如果三级缓存找到了则调用Adapter.bindViewHolder来绑定内容,然后返回。如果三级缓存没有找到,那么就通过Adapter.createViewHolder创建一个ViewHolder,然后调用Adapter.bindViewHolder绑定其内容,然后返回为Recycler。【参见后文:2. 缓存机制】
  6. 一直重复步骤3-5,知道创建的ViewHolder填满了整个RecyclerView为止。

3. 缓存机制

3.1 源码简析

RecyclerView在Recyler里面实现ViewHolder的缓存,Recycler里面的实现缓存的主要包含以下5个对象:

  • ArrayList mAttachedScrap:未与RecyclerView分离的ViewHolder列表,如果仍依赖于 RecyclerView (比如已经滑动出可视范围,但还没有被移除掉),但已经被标记移除的 ItemView 集合会被添加到 mAttachedScrap 中
    • 按照id和position来查找ViewHolder
  • ArrayList mChangedScrap:表示数据已经改变的viewHolder列表,存储 notifXXX 方法时需要改变的 ViewHolder,匹配机制按照position和id进行匹配
  • ArrayList mCachedViews:缓存ViewHolder,主要用于解决RecyclerView滑动抖动时的情况,还有用于保存Prefetch的ViewHoder
    • 最大的数量为:mViewCacheMax = mRequestedCacheMax + extraCache(extraCache是由prefetch的时候计算出来的)
  • ViewCacheExtension mViewCacheExtension:开发者可自定义的一层缓存,是虚拟类ViewCacheExtension的一个实例,开发者可实现方法getViewForPositionAndType(Recycler recycler, int position, int type)来实现自己的缓存。
  • mRecyclerPool ViewHolder缓存池,在有限的mCachedViews中如果存不下ViewHolder时,就会把ViewHolder存入RecyclerViewPool中。
    • 按照Type来查找ViewHolder
    • 每个Type默认最多缓存5个
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;

3.2 缓存机制图解

RecyclerView在设计的时候讲上述5个缓存对象分为了3级。每次创建ViewHolder的时候,会按照优先级依次查询缓存创建ViewHolder。每次讲ViewHolder缓存到Recycler缓存的时候,也会按照优先级依次缓存进去。三级缓存分别是:

  • 一级缓存:返回布局和内容都都有效的ViewHolder
    • 按照position或者id进行匹配
    • 命中一级缓存无需onCreateViewHolder和onBindViewHolder
    • mAttachScrap在adapter.notifyXxx的时候用到
    • mChanedScarp在每次View绘制的时候用到,因为getViewHolderForPosition非调用多次,后面将
    • mCachedView:用来解决滑动抖动的情况,默认值为2
  • 二级缓存:返回View
    • 按照position和type进行匹配
    • 直接返回View
    • 需要自己继承ViewCacheExtension实现
    • 位置固定,内容不发生改变的情况,比如说Header如果内容固定,就可以使用
  • 三级缓存:返回布局有效,内容无效的ViewHolder
    • 按照type进行匹配,每个type缓存值默认=5
    • layout是有效的,但是内容是无效的
    • 多个RecycleView可共享,可用于多个RecyclerView的优化

3.3 实例讲解

实例解释:

(1)由于ViewCacheExtension在实际使用的时候较少用到,因此本例中忽略二级缓存

(2)mChangedScrap和mAttchScrap是RecyclerView内部控制的缓存,本例暂时忽略。

(3)为了简化问题,暂时不考虑PreFetch的情况

(4)图片解释:

  • RecyclerView包含三部分:已经出屏幕,在屏幕里面,即将进入屏幕,我们滑动的方向是向上
  • RecyclerView包含三种Type:1,2,3。屏幕里面的都是Type=3
  • 红色的线代表已经出屏幕的ViewHoder与Recycler的交互情况
  • 绿色的线代表,即将进入屏幕的ViewHoder进入屏幕时候,ViewHolder与Recycler的交互情况
  • 紫色的线代表mCacheView进入缓存池的情况

出屏幕时候的情况-mCacheViews未满

  1. 当ViewHolder(position=0,type=1)出屏幕的时候,由于mCacheViews是空的,那么就直接放在mCacheViews里面(从0-N是由老到新)。此时ViewHolder在mCacheViews里面布局和内容都是有效的,因此可以直接复用。
  2. ViewHolder(position=1,type=2)同步骤1

出屏幕时候的情况-mCacheViews已经满

  1. 当ViewHolder(position=2,type=1)出屏幕的时候由于一级缓存mCacheViews已经满了,因此然后移除mCacheViews里面最老的ViewHolder(position=0,type=1)到RecyclePool中,然后将ViewHolder(position=2,type=1)存入mCacheViews。此时被移除到RecyclePool的ViewHolder的内容会被标记为无效,当其复用的时候需要再次通过Adapter.bindViewHolder来绑定内容。
  2. ViewHolder(position=3,type=3)同步骤3

进屏幕时候的情况

  1. 当ViewHolder(position=3,type=3)进入屏幕绘制的时候,由于Recycler的mCacheViews里面找不到position匹配的View,同时RecyclerPool里面找不到type匹配的View,因此,其只能通过adapter.createViewHolder来创建ViewHolder,然后通过adapter.bindViewHolder来绑定内容。
  2. 当ViewHolder(position=11,type=1)进入屏幕的时候,发现ReccylerPool里面能找到type=1的缓存,因此直接从ReccylerPool里面取来使用。由于内容是无效的,因此还需要调用bindViewHolder来绑定布局。同时ViewHolder(position=4,type=3)需要出屏幕,会经历步骤3回收的过程
  3. ViewHolder(position=12,type=3)同步骤6

屏幕往下拉ViewHoder(position=1)进入屏幕的情况

  1. 由于mCacheView里面的有position=1的ViewHolder与之匹配,直接返回。由于内容是有效的,因此无需再次绑定内容
  2. ViewHolder(position=0)同步骤8

4. RecyclerView性能优化方向总结

这里写图片描述