RecycleView 缓存机制

参考文献

您可以先看看下面的文章后,再看博主的也许有更大的收货.

  1. 每日一问 | RecyclerView的多级缓存机制,每级缓存到底起到什么样的作用?

  2. 深入理解 RecyclerView 的缓存机制

  3. RecyclerView缓存原理,有图有真相

前言

  • 本文所分析的源码版本:
    androidx.recyclerview:recyclerview:1.1.0

  • 缓存本质思想:
    缓存是一种空间换时间算法思想.

  • recyclerview缓存了什么:
    缓存了ViewHolder对象.

  • recyclerview缓存的目的:

    1. 复用视图和数据的绑定(减少onBindViewHolder)
    2. 复用ViewHolder对象(减少onCreateViewHolder调用)

上面说到缓存的对象是ViewHold,所以我们必须弄清楚什么是ViewHolder,他解决了什么问题?

ViewHolder基础功能:

  • 1 他是View的持有者,在构造时直接findviewById,减少后面再次寻找id控件所带来的的时间损失(寻找id采用深度优先算法)
class MyViewHold constructor(val view: View) : RecyclerView.ViewHolder(view) {
  //构造时直接寻找一个子控件
  val tv: TextView = view.findViewById(R.id.tv);
}
  • 2 封装一些基础操作函数
class MyViewHold constructor(val view: View) : RecyclerView.ViewHolder(view) {
  //构造时直接寻找一个子控件
  val tv: TextView = view.findViewById(R.id.tv);

  /**
   * 更新信息
   */
  fun upInfo(msg:String){
    tv.setText(msg)
  }
}
  • 3 关联数据和视图的中间组件
 class RVAdapter(val data: List<String>) : RecyclerView.Adapter<MyViewHold>(){
  //更新数据和视图的绑定关系
  override fun onBindViewHolder(
    holder: MyViewHold,
    position: Int
  ) {
       holder.upInfo("${data[position]}")
  }
}

网上有无数的文章说到四级缓存,也就是说会有四个地方会缓存ViewHolder,既然分了四个地方那么必然存在不同的原因,我们先不深究这个,我们首先看下获取ViewHolder的流程:

在这里插入图片描述

可以看到ViewHolder会被缓存到四处.

一级缓存

假设我们RecyclerView被系统强制刷新了界面(比如系统的vsync信号,或者invalidate()触发),可是数据没有任何变化,我们的所有ViewHolder还需要重新绑定数据是多余的.(也就是说这种情况不应该调用onBindViewHolderonCreateViewHolder).

这个问题解决是交付给一级缓存的mAttachedScrap所实现的.

在这里插入图片描述
RecyclerView在数据未改动时刷新界面:

在这里插入图片描述
上面是在数据干净时的情况,也就是所有的ViewHolder是干净的.假设我们我们Item4是脏的怎么办?比如item4显示张三现在要显示李四,mChangedScrap就是用来存储屏幕内脏ViewHolder的情况用于做预布局.这里又扯到预布局的概念具体的可以看上文的参考.
我们这里简单做下简单描述:

RecyclerView会做一次预布局最终布局.预布局就是用当前存在脏数据缓存摆放子view方便得出当前状态,最后和最终布局做比较好执行动画等操作.

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

通过两个布局我们的RecyclerView会感知出要执行Item4的变化动画,后面执行动画,在执行动画后会将mChangedScrap的数据放入四级缓存/RecycledViewPool中,

注意mChangedScrap缓存的对象会在预布局最终布局放后移动到四级缓存中,所以mChangedScrap在当次刷新中是不会复用,只会保留在下次复用.
在这里插入图片描述

可能你会问,我们在代码中如何复现这个现象?

  //修改张三为李四
  dataList[3] = "李四"
  //标记第四个元素是脏的,其他item是干净
  rvAdapter.notifyItemChanged(3)

当你调用后你会发现第四个VIewHolder会调用onBindViewHolder.

  • 总结:
  • 一级缓存用于缓存屏幕的ViewHolder.
  • mChangedScrap用于缓存改变数据的ViewHolder,主要用于预测布局,布局
  • mAttachedScrap用于缓存位改未变数据的ViewHolder

注意:
mAttachedScrap存放的元素并不是一定不会调用onBindViewHolder.
在如下情况是会将屏幕元素的Item放入mAttachedScrap然后调用他们的数据绑定函数onBindViewHolder.

情况:一个adapter设置有了有稳定id的选项setHasStableIds(true),然后直接调用notifyDataSetChanged.

二级缓存

我们后很多时候会有一种习惯:向上滑动屏幕推出Item1,然后又向下滑动推回Item1,笔者就有这种习惯.假设我们能有一套机制快速恢复少量的Item(不需要绑定数据onBindViewHolder),对于这类行为是非常有帮助.而mCachedViews就是帮助我们做这样的事情.

在这里插入图片描述

在这里插入图片描述
mCachedViews大小很小默认数量为2(由androidx.recyclerview.widget.RecyclerView.Recycler#mRequestedCacheMax变量初始化)

  • 二级缓存还起到预加载的作用:
    在这里插入图片描述
    当你滑动Item5时会预加载Item6到二级缓存中(可能会创建ViewHolder,或者复用其他不可见ViewHolder),当滑动Item6时已经提前绑定了数据所以用户可以更丝滑体验滑动.

三级缓存

ViewCacheExtension这个我实在没深刻领会Google工程师的设计,而且有点难用,为了不误导大家这里就跳过.
我看了下很多开源项目也都没有实现

四级缓存

我们上面的讨论的一级二级在平常已经足够我们使用了.如果二级缓存的数据是脏的直接调用onBindViewHolder即可,何必又重新多出一级缓存?把这个弄明白才是根本,相比起深入源码无法自拔更加能领略作者的思想境界.

我认为有如下几个方面需要额外多出一级缓存的必要:

  • 1 避免预加载时,一级二级缓存的浪费
  • 2 多个RecyclerView之间的ViewHolder共享

避免在一级二级缓存的浪费
我们首先查看这个问题根本原因

  • 步骤1
    在这里插入图片描述

  • 步骤2
    在这里插入图片描述

  • 步骤3
    此时我们假设我们屏幕完全划入Item6.item2滑出屏幕,此时Item1,和Item7已经被被二级缓存存储,且数量已经到底上限(二级缓存默认情况下上限为2),item2被缓存到哪里呢?或者用存丢弃掉item1,item7?

在这里插入图片描述
所以此时我们的四级缓存出现了.我们把Item1丢入四级缓存,接着将item2放入二级缓存.

在这里插入图片描述

此时多出的item1可以在其他RecyclerView中复用,或者在预加载Item8的时候复用,所以在上面的情况下RecyclerView会最多创建7个ViewHolder然后进行循环复用.

这里给出一些会放入四级缓存的途径:

  1. 二级缓存超限放入
  2. 一级缓存的mAttachScrap在执行完动画后放入
  3. 其他情况的脏ViewHolder放入

四级缓存数据结构

相比起一二级缓存的List结构来说,四级缓存结构复杂.四级缓存是根据ViewHolder类别存储(ViewHolder.getItemViewType).每个类别默认情况存储5为上限.

在这里插入图片描述

 //RecyclerView.java
 public static class RecycledViewPool {
 	//标记每个类别的ViewHolder存储上限数量
 	private static final int DEFAULT_MAX_SCRAP = 5;
	//用于存储每个每个类别ViewHolder
	SparseArray<ScrapData> mScrap = new SparseArray<>();

	static class ScrapData {
            final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
            int mMaxScrap = DEFAULT_MAX_SCRAP;
            long mCreateRunningAverageNs = 0;
            long mBindRunningAverageNs = 0;
        }
      //获取指定类别的ScrapData
      private ScrapData getScrapDataForType(int viewType) {
            ScrapData scrapData = mScrap.get(viewType);
            if (scrapData == null) {
                scrapData = new ScrapData();
                mScrap.put(viewType, scrapData);
            }
            return scrapData;
        }   
    //放入指定的元素    
    public void putRecycledView(ViewHolder scrap) {
            final int viewType = scrap.getItemViewType();
            
            final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
            //超过上限就不放入
            if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
                return;
            }
            //给ViewHolder重置一些标志位方便下次使用
            scrap.resetInternal();
            //放入集合中
            scrapHeap.add(scrap);
        }    
 }

源码分析

看完上面的即便你不看源码也可以很明白四级缓存的作用了.
RecyclerView是一个view,所以必然经过measure,layout,draw,我们这里忽略drawmeasure函数,直接看layout函数调用链即可.

//RecyclerView.java
class RecyclerView{
   protected void onLayout(boolean changed, int l, int t, int r, int b) {
	 dispatchLayout();
	}
	void dispatchLayout() {
	//..略,这里其他布局函数可能涉及预测布局,如果你有兴趣可以可看dispatchLayoutStep1 dispatchLayoutStep3
         dispatchLayoutStep2();
    //..略
    }
     private void dispatchLayoutStep2() {
       // LayoutManager mLayout; 我们这里以LinearLayoutManager为例
       mLayout.onLayoutChildren(mRecycler, mState);
    }
}

上面RecyclerView会把布局责任托付给LayoutManager(其他绘制和测量也是如此).
我们这里以LinearLayoutManager为例

//LinearLayoutManager.java
class LinearLayoutManager{
	public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
		  //略	
		  fill(recycler, mLayoutState, state, false);
	}
   int fill(/**...略..**/) {
           
           //循环填充view到RecyclerView中
             while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {          
           			 layoutChunk(recycler, state, layoutState, layoutChunkResult);
            }
     }
      void layoutChunk(/**...略..**/) {
      		//...略...
      	//从ViewHolder取出的view
      	//layoutState对象为LayoutState
      	 View view = layoutState.next(recycler);
      	 
      	 //放入RecyclerView中
      	 addView(view);
		      		//...略...
      } 
}
//LinearLayoutManager.java

 static class LayoutState {
 	 View next(RecyclerView.Recycler recycler) {
           //从recycler获取ViewHolder,返回,可以看到总算到缓存核心类Recycler
         final View view = recycler.getViewForPosition(mCurrentPosition);
          return view;
    }
 }
//RecyclerView.java
public final class Recycler {
 	public View getViewForPosition(int position) {
          return getViewForPosition(position, false);
    }
    View getViewForPosition(int position, boolean dryRun) {
     //缓存核心函数			
     return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
    }
}

可以看到最终缓存回去Recycler.tryGetViewHolderForPositionByDeadline获取,

  ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
           
            boolean fromScrapOrHiddenOrCache = false;
            ViewHolder holder = null;
            // 0)预布局相关,tryGetViewHolderForPositionByDeadline会在预布局中调用一次,和最终布局调用一次
            //  预布局可以辅助一些动画预测,这里我们不需要深究,预布局在dispatchLayoutStep1调用
            // 如果是预布局isPreLayout为true.
            // 
            if (mState.isPreLayout()) {
                //从mChangedScrap取出ViewHolder,后面执行动画时会放入四级缓存中
                //注意这个holder不会在本次最终布局进行复用,所以脏的ViewHolder,会被一个新的ViewHolder取代,您可以代码验证
                holder = getChangedScrapViewForPosition(position);
                fromScrapOrHiddenOrCache = holder != null;
            }

            // 1) Find by position from scrap/hidden list/cache
            if (holder == null) {
                //从mAttachedScrap和mCachedViews寻找    
                holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
                //略
            }

            if (holder == null) {
                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                
                final int type = mAdapter.getItemViewType(offsetPosition);
                //如果设置稳定id,那么会再次通过mCachedViews和mAttachedScrap去寻找
                //只不过内部寻找的时候主要同过ViewHolder的id去寻找.你一定会疑惑和上面有什么区别
                //getScrapOrCachedViewForId内部会用id和ViewHolder类别比较得到一个符合的ViewHolder
                //而getScrapOrHiddenOrCachedHolderForPosition仅通过position
                if (mAdapter.hasStableIds()) {
                    //会去mCachedViews和mAttachedScrap去寻找
                    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                            type, dryRun);
                    //更新holder信息
                    if (holder != null) {
                        // update position
                        holder.mPosition = offsetPosition;
                        fromScrapOrHiddenOrCache = true;
                    }
                }
                //通过三级缓存去寻找  
                if (holder == null && mViewCacheExtension != null) {
                    // We are NOT sending the offsetPosition because LayoutManager does not
                    // know it.
                    final View view = mViewCacheExtension
                            .getViewForPositionAndType(this, position, type);
                    if (view != null) {
                        holder = getChildViewHolder(view);
                        if (holder == null) {
                            throw new IllegalArgumentException("getViewForPositionAndType returned"
                                    + " a view which does not have a ViewHolder"
                                    + exceptionLabel());
                        } else if (holder.shouldIgnore()) {
                            throw new IllegalArgumentException("getViewForPositionAndType returned"
                                    + " a view that is ignored. You must call stopIgnoring before"
                                    + " returning this view." + exceptionLabel());
                        }
                    }

                }

                //四级缓存区遍历
                if (holder == null) { // fallback to pool
                    //
                    holder = getRecycledViewPool().getRecycledView(type);
                    if (holder != null) {
                        holder.resetInternal();
                        if (FORCE_INVALIDATE_DISPLAY_LIST) {
                            invalidateDisplayListInt(holder);
                        }
                    }
                }

                if (holder == null) {
                    //都找不到自己创建一个返回
                    holder = mAdapter.createViewHolder(RecyclerView.this, type);
                    

                   
                }
            }
 		if (mState.isPreLayout() && holder.isBound()) {
               	//略
            } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
              //函数内部会执行后holder.bindViewHolder操作
                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
            }

            //略
            return holder;
        }

Stable Id 相关问题

setHasStableIds(true)设置之后除了可以显示相关动画之外还有什么特性?

特点1

RecyclerView在具有稳定id的情况下,notifyDataSetChanged会把所有元素放入1级缓存/mAttachedScrap中,如果没有稳定id会放入四级缓存中.还记得我们说过四级缓存仅能存储五个同类型的ViewHolder吗?

在这里插入图片描述

如果没有稳定id在上面的情况刷新后仅有五个ViewHolder会被复用,其余的ViewHolder又会重新创建.

onCreateViewHolder  创建 1                                
onBindViewHolder position 0             
onCreateViewHolder  创建 2                                
onBindViewHolder position 1                    
onCreateViewHolder  创建 3                                
onBindViewHolder position 2                    
onCreateViewHolder  创建 4                                
//..略                                                   
onCreateViewHolder  创建 16                               
onBindViewHolder position 15                   
                                                        
------ 无稳定id刷新  notifyDataSetChanged  -----     
        
onBindViewHolder position 0                    
onBindViewHolder position 1                    
onBindViewHolder position 2                    
onBindViewHolder position 3                    
onBindViewHolder position 4                    
onCreateViewHolder  创建 17                               
onBindViewHolder position 5                    
onCreateViewHolder  创建 18                               
onBindViewHolder position 6                    
onCreateViewHolder  创建 19                               
onBindViewHolder position 7                    
onCreateViewHolder  创建 20                               
onBindViewHolder position 8                     
onCreateViewHolder  创建 21                               
onBindViewHolder position 9                    
onCreateViewHolder  创建 22                               
onBindViewHolder position 10                   
onCreateViewHolder  创建 23                               
onBindViewHolder position 11                  
onCreateViewHolder  创建 24                               
onBindViewHolder position 12                   
onCreateViewHolder  创建 25                               
onBindViewHolder position 13                   
onCreateViewHolder  创建 26                               
onBindViewHolder position 14                   
onCreateViewHolder  创建 27                               
onBindViewHolder position 15                   
                                                        

特点2

RecyclerView在复用会尽量复用id相同的ViewHolder(上面源码分析你可以看到if (mAdapter.hasStableIds())的代码就是),所以你可以利用这个特性进行判断加大局部刷新成功率.

class RVAdapter() : RecyclerView.Adapter<MyViewHold>(){

    override fun onBindViewHolder(holder: MyViewHold, position: Int, payloads: MutableList<Any>) {
			//因为recyclerview尽可能保证复用,所以很有可能命中
        if (holder.view.tag == "标识符") {

        } else {
            super.onBindViewHolder(holder, position, payloads)
        }

    }
}    

如果你想了解相关源码可以直接看RecyclerViewDataObserver.onChanged这个函数会在adapter数据改变后回调.这个类是RecyclerView的内部类,比较简单.本来想写的,谁知道画图画了半天.

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值