谈谈RecyclerView中的缓存

Android深入理解RecyclerView的缓存机制

RecyclerView在项目中的使用已经很普遍了,可以说是项目中最高频使用的一个控件了。除了布局灵活性、丰富的动画,RecyclerView还有优秀的缓存机制,本文尝试通过源码深入了解一下RecyclerView中的缓存机制。

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

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

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

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

image

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。
  6. 一直重复步骤3-5,知道创建的ViewHolder填满了整个RecyclerView为止。

RecyclerView滑动时会触发onTouchEvent#onMove,回收及复用ViewHolder在这里就会开始。我们知道设置RecyclerView时需要设置LayoutManager,LayoutManager负责RecyclerView的布局,包含对ItemView的获取与复用。以LinearLayoutManager为例,当RecyclerView重新布局时会依次执行下面几个方法:

  • onLayoutChildren():对RecyclerView进行布局的入口方法
  • fill(): 负责对剩余空间不断地填充,调用的方法是layoutChunk()
  • layoutChunk():负责填充View,该View最终是通过在缓存类Recycler中找到合适的View的

上述的整个调用链:onLayoutChildren()->fill()->layoutChunk()->next()->getViewForPosition(),getViewForPosition()即是是从RecyclerView的回收机制实现类Recycler中获取合适的View,下面主要就来从看这个Recycler#getViewForPosition()的实现。

@NonNull
public View getViewForPosition(int position) {
    return getViewForPosition(position, false);
}
View getViewForPosition(int position, boolean dryRun) {
    return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}

//根据传入的position获取ViewHolder 
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
        boolean dryRun, long deadlineNs) {
    ---------省略----------
    boolean fromScrapOrHiddenOrCache = false;
    ViewHolder holder = null;
    //预布局 属于特殊情况 从mChangedScrap中获取ViewHolder
    if (mState.isPreLayout()) {
        holder = getChangedScrapViewForPosition(position);
        fromScrapOrHiddenOrCache = holder != null;
    }
    if (holder == null) {
        //1、尝试从mAttachedScrap中获取ViewHolder,此时获取的是屏幕中可见范围中的ViewHolder
        //2、mAttachedScrap缓存中没有的话,继续从mCachedViews尝试获取ViewHolder
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
     ----------省略----------
    }
    if (holder == null) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        ---------省略----------
        final int type = mAdapter.getItemViewType(offsetPosition);
        //如果Adapter中声明了Id,尝试从id中获取,这里不属于缓存
        if (mAdapter.hasStableIds()) {
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                    type, dryRun);
        }
        if (holder == null && mViewCacheExtension != null) {
            3、从自定义缓存mViewCacheExtension中尝试获取ViewHolder,该缓存需要开发者实现
            final View view = mViewCacheExtension
                    .getViewForPositionAndType(this, position, type);
            if (view != null) {
                holder = getChildViewHolder(view);
            }
        }
        if (holder == null) { // fallback to pool
            //4、从缓存池mRecyclerPool中尝试获取ViewHolder
            holder = getRecycledViewPool().getRecycledView(type);
            if (holder != null) {
                //如果获取成功,会重置ViewHolder状态,所以需要重新执行Adapter#onBindViewHolder绑定数据
                holder.resetInternal();
                if (FORCE_INVALIDATE_DISPLAY_LIST) {
                    invalidateDisplayListInt(holder);
                }
            }
        }
        if (holder == null) {
            ---------省略----------
          //5、若以上缓存中都没有找到对应的ViewHolder,最终会调用Adapter中的onCreateViewHolder创建一个
            holder = mAdapter.createViewHolder(RecyclerView.this, type);
        }
    }
    boolean bound = false;
    if (mState.isPreLayout() && holder.isBound()) {
        holder.mPreLayoutPosition = position;
    } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        //6、如果需要绑定数据,会调用Adapter#onBindViewHolder来绑定数据
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }
    ----------省略----------
    return holder;
}

上述逻辑用流程图表示:

3. 缓存机制

3.1 源码简析

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

  • ArrayList mAttachedScrap:该层缓存目的是在调用notfyXxx时未改变的item,以及影响RecyclerView重新绘制的情况。mChangedScrap和mAttachedScrap可以看做是一个层级,都是屏幕上可见itemView,只不过区分了状态(改变和未改变)。
  • ArrayList mChangedScrap:该层缓存目的是为了当调用notifyItemChanged(pos),notifyItemRangeChanged(pos,count)后该位置信息发生改变的缓存,一般用于change动画,注意mChangedScrap并不是说存储改变的位置并直接复用,而是在预布局时存储改变的holder,重新创建新holder并绑定数据来充当改变位置的数据刷新,然后根据新老holder执行change动画。动画执行完毕后新的holder会被缓存到mRecyclerPool中。那如何复用notifyItemChanged(pos)改变的holder呢?
  • ArrayList mCachedViews:作用在滑动,当滑进屏幕或滑出屏幕,为了避免多次bind,是一个大小为2的List。还有用于保存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 mAttachedScrap = new ArrayList<>();

ArrayList mChangedScrap = null;

  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;
                //mCachedViews的默认大小
        static final int DEFAULT_CACHE_SIZE = 2;

3.2 缓存机制图解

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

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

3.3 实例讲解

image
void recycleViewHolderInternal(ViewHolder holder) {
            if (forceRecycle || holder.isRecyclable()) {
                //只有缓存数量大于0,才会放到mCachedViews里
                if (mViewCacheMax > 0
                        && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                        | ViewHolder.FLAG_REMOVED
                        | ViewHolder.FLAG_UPDATE
                        | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
                    // Retire oldest cached view
                    int cachedViewSize = mCachedViews.size();
                    if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                        //缓存满了移除第一个,放到缓存池中
                        recycleCachedViewAt(0);
                        cachedViewSize--;
                    }

                    int targetCacheIndex = cachedViewSize;
                    if (ALLOW_THREAD_GAP_WORK
                            && cachedViewSize > 0
                            && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
                        // when adding the view, skip past most recently prefetched views
                        int cacheIndex = cachedViewSize - 1;
                        while (cacheIndex >= 0) {
                            int cachedPos = mCachedViews.get(cacheIndex).mPosition;
                            if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
                                break;
                            }
                            cacheIndex--;
                        }
                        targetCacheIndex = cacheIndex + 1;
                    }
                    //加入缓存
                    mCachedViews.add(targetCacheIndex, holder);
                    cached = true;
                }
                if (!cached) {
                    //放到缓存池中
                    addViewHolderToRecycledViewPool(holder, true);
                    recycled = true;
                }
            } else {
                // NOTE: A view can fail to be recycled when it is scrolled off while an animation
                // runs. In this case, the item is eventually recycled by
                // ItemAnimatorRestoreListener#onAnimationFinished.

                // TODO: consider cancelling an animation when an item is removed scrollBy,
                // to return it to the pool faster
                if (DEBUG) {
                    Log.d(TAG, "trying to recycle a non-recycleable holder. Hopefully, it will "
                            + "re-visit here. We are still removing it from animation lists"
                            + exceptionLabel());
                }
            }
            // even if the holder is not removed, we still call this method so that it is removed
            // from view holder lists.
            mViewInfoStore.removeViewHolder(holder);
            if (!cached && !recycled && transientStatePreventsRecycling) {
                holder.mBindingAdapter = null;
                holder.mOwnerRecyclerView = null;
            }
        }

4. 实战应用

1.使用自定义ViewCacheExtension

//把缓存池中该类型的size设为0 
viewPool.setMaxRecycledViews(CacheUtil.TYPE_SPECIAL, 0)
 recyclerView.setViewCacheExtension(object : RecyclerView.ViewCacheExtension() {
      override fun getViewForPositionAndType(recycler: RecyclerView.Recycler, position: Int, type: Int): View? {
             if (type != CacheUtil.TYPE_SPECIAL) {
                 return null
             }
             if (adapter.cacheView.isEmpty()) {
                 return null
             }
              return adapter.cacheView[0]
       }
})
 class RecyclerViewAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
        val dataList = ArrayList<Int>()
        val cacheView = ArrayList<View>()
        fun setData(list: List<Int>) {
            dataList.addAll(list)
            notifyDataSetChanged()
        }

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
            return when (viewType) {
                CacheUtil.TYPE_SPECIAL -> {
                    val view = LayoutInflater.from(parent.context)
                        .inflate(R.layout.item_type_special, parent, false)
                    cacheView.add(view)
                    SpecialViewHolder(view)
                }
            else -> EmptyViewHolder(TextView(parent.context))
        }
}  

使用自定义ViewCacheExtension后,view离屏后再回来不会走onBindViewHolder()方法。

2.让某个ViewHolder不缓存

holder.setIsRecyclable(false),这样的话每次都会走onCreateViewHolder()和onBindViewHolder()方法

3.缓存优化

1.提前初始化viewHolder,放到缓存池中

viewPool.putRecycledView(adapter.onCreateViewHolder(recyclerView, 1))

2.提前初始化view,在onCreateViewHolder的时候去取view

3.自定义ViewCacheExtension

4.适当的增加cacheSize

4.公用缓存池,比如多个viewPager+fragment场景使用,或者全局单利缓存池,感觉用户不大。

5. 注意事项

1.mChangedScrap什么时候会有值

void scrapView(View view) {
            final ViewHolder holder = getChildViewHolderInt(view);
            if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
                    || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
                holder.setScrapContainer(this, false);
                mAttachedScrap.add(holder);
            } else {
                if (mChangedScrap == null) {
                    mChangedScrap = new ArrayList<ViewHolder>();
                }
                holder.setScrapContainer(this, true);
                mChangedScrap.add(holder);
            }
        }

有2中做法有值

第一种

 val anim = DefaultItemAnimator()
 anim.supportsChangeAnimations = true
 recyclerView.itemAnimator = anim
 adapter.notifyItemChanged(1)
 adapter.notifyItemRangeChanged(1, 3)

第二种

 class MyItemAnim : DefaultItemAnimator() {
        override fun canReuseUpdatedViewHolder(viewHolder: RecyclerView.ViewHolder): Boolean {
            return false
        }

        override fun canReuseUpdatedViewHolder(viewHolder: RecyclerView.ViewHolder, payloads: MutableList<Any>): Boolean {
            return false
        }
    }
adapter.notifyItemChanged(1)
adapter.notifyItemChanged(1,"哈哈") 
adapter.notifyItemRangeChanged(1, 3, "哈哈")

2.recyclerView.setItemViewCacheSize(0)代表缓存失效吗?

不会,因为prefetch(GapWorker中的一个方法)之后mViewCacheMax会变成mRequestedCacheMax + extraCache

 @Override
 public void addPosition(int layoutPosition, int pixelDistance) {
       mCount++;
}

 void collectPrefetchPositionsFromView(RecyclerView view, boolean nested) {
        mCount = 0;
       //layout.mPrefetchMaxCountObserved默认值0
       if (mCount > layout.mPrefetchMaxCountObserved) {
             layout.mPrefetchMaxCountObserved = mCount;
             layout.mPrefetchMaxObservedInInitialPrefetch = nested;
             view.mRecycler.updateViewCacheSize();
        }
}

public void setViewCacheSize(int viewCount) {
       mRequestedCacheMax = viewCount;
       updateViewCacheSize();
}

 void updateViewCacheSize() {
      int extraCache = mLayout != null ? mLayout.mPrefetchMaxCountObserved : 0;
      mViewCacheMax = mRequestedCacheMax + extraCache;

      // first, try the views that can be recycled
      or (int i = mCachedViews.size() - 1;
             i >= 0 && mCachedViews.size() > mViewCacheMax; i--) {
           recycleCachedViewAt(i);
      }
}

有2种方式可以让缓存失效

第一种

recyclerView.setItemViewCacheSize(-1)

第二种

recyclerView.setItemViewCacheSize(0)

layoutManager.isItemPrefetchEnabled = false

设置不缓存后,来回滑动让view进入屏幕离开屏幕,viewHolder的item时会多次走onBindViewHolder()方法。

6. RecyclerView性能优化方向总结

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

推荐阅读更多精彩内容