RecycleView相关面试题

  • 讲一下RecyclerView的缓存机制,滑动10个,再滑回去,会有几个执行onBindView。缓存的是什么?cachedView会执行onBindView吗?

  • RecyclerView预取机制

  • 如何实现RecyclerView的局部更新,用过payload吗,notifyItemChange方法中的参数?

  • RecyclerView嵌套RecyclerView滑动冲突,NestScrollView嵌套RecyclerView。

  • 说说RecyclerView性能优化。

更多面试题详见:

RecyclerView问题汇总:

https://juejin.cn/post/6844903837724213256

listview缓存请看: listview优化和详解

RecycleView 和 ListView对比:

  1. 使用方法上

ListView:继承重写 BaseAdapter,自定义 ViewHolder 与 converView优化。

RecyclerView: 继承重写 RecyclerView.Adapter 与 RecyclerView.ViewHolder。设置 LayoutManager 来展示不同的布局样式

区别:

ViewHolder的编写规范化,ListView是需要自己定义的,而RecyclerView是规范好的;
RecyclerView复用item全部搞定,不需要像ListView那样setTag()与getTag();
RecyclerView多了一些LayoutManager工作,但实现了布局效果多样化;

2. 动画api

在RecyclerView中自带动画效果,例如:notifyItemChanged(), notifyDataInserted(), notifyItemMoved()等等;同时内置有许多动画API,如果需要自定义动画效果,可以通过实现(RecyclerView.ItemAnimator类)完成自定义动画效果,然后调用RecyclerView.setItemAnimator();

但是ListView并没有实现动画效果,需要在Adapter自己自定义;

3. 缓存区别

ListView和RecyclerView最大的区别在于数据源改变时的缓存的处理逻辑,ListView是二级缓存,而RecyclerView则是更加灵活地采用了四级缓存。

recycleview有四级缓存:

  1. mAttachedScrap(屏幕内),用于屏幕内itemview快速重用,不需要重新createView和bindView

  1. mCacheViews(屏幕外),保存最近移出屏幕的ViewHolder,包含数据和position信息,复用时必须是相同位置的ViewHolder才能复用,应用场景在那些需要来回滑动的列表中,当往回滑动时,能直接复用ViewHolder数据,不需要重新bindView。

  1. mViewCacheExtension(自定义缓存),不直接使用,需要用户自定义实现,默认不实现。

  1. mRecyclerPool(缓存池),当cacheView满了后或者adapter被更换,将cacheView中移出的、ViewHolder放到Pool中,放之前会把ViewHolder数据清除掉,所以复用时需要重新bindView。这个缓存池是一个二维数组 外部是ScrapData 的SparseArray数组,内部是ArrayList数组。

通过View view = recycler.getViewForPosition(position)可以实现复用,根据源码可知,在RecyclerView中,总共有四级缓存,优先级:
1. mAttachedScrap>mCachedViews>mViewCacheExtension>mRecyclerPool。
2. mAttachedScrap:只保存当前屏幕中detach的ViewHolder,在重新布局时复用。
3. mCachedViews:缓存的是刚从RecyclerView中移除的ViewHolder(通过removeAndRecycleView(view, recycler)方法), 在复用时需要position或id匹配才能复用,所以只有在来回滑动过程中才会复用mCachedViews中的ViewHolder。如果不能匹配就需要从mRecyclerPool中取出ViewHolder并重新绑定数据。
4. 复用mAttachedScrap、mCachedViews中的ViewHolder是需要精确匹配的,如果能匹配上可直接使用不需绑定数据,如果不能精确匹配,即使mAttachedScrap、mCachedViews中有缓存也不能取出使用,只能从mRecyclerPool中取出使用,并且需重绑数据。如果mRecyclerPool中没有缓存就需要调用onCreateViewHolder进行创建。

RecycledViewPool深入理解:

在Scrap、CacheView、ViewCacheExtension都不愿意回收的时候,都会丢到RecycledViewPool中回收,所以RecycledViewPool是Recycler的终极回收站。
RecycledViewPool实际上是以SparseArray嵌套一个ArraryList的形式保存ViewHolder的,因为RecycledViewPool保存的ViewHolder是以itemType来区分的。这样方便不同的itemType保存不同的ViewHolder。 它在回收的时候只是回收该viewType的ViewHolder对象,并没有保存原来的数据信息,在复用的时候需要重新走onBindViewHolder()方法重新绑定数据


可以看出,RecycledViewPool中定义了SparseArray mScrap,它是一个根据不同itemType来保存静态类ScrapData对象的SparseArray,ScrapData中包含了ArrayList mScrapHeap ,mScrapHeap是保存该itemType类型下ViewHolder的ArrayList。

缓存池定义了默认的缓存大小DEFAULT_MAX_SCRAP = 5,这个数量不是说整个缓存池只能缓存这多个ViewHolder,而是不同itemType的ViewHolder的list的缓存数量,即mScrap的数量,说明最多只有5组不同类型的mScrapHeap。mMaxScrap = DEFAULT_MAX_SCRAP说明每种不同类型的ViewHolder默认保存5个,当然mMaxScrap的值是可以设置的。这样RecycledViewPool就把不同ViewType的ViewHolder按类型分类缓存起来。
————————————————
原文链接: https://blog.csdn.net/qjyws/article/details/123237071

从缓存池RecycledViewPool中根据布局类型获取对应的ViewHolder集合(最大长度为5),然后再从集合中查找缓存是否存在。

RecycleView相关面试题_第1张图片

RecyclerViewPool类似于HashMap的数据结构,根据不同的布局类型viewType获取缓存Holder。

四级缓存完整存取缓存流程是:

  1. 保存缓存流程:

  • 插入或是删除itemView时,先把屏幕内的ViewHolder保存至AttachedScrap中

  • 滑动屏幕的时候,先消失的itemview会保存到CacheView,CacheView大小默认是2,超过数量的话按照先入先出原则,移出头部的itemview保存到RecyclerPool缓存池(如果有自定义缓存就会保存到自定义缓存里),RecyclerPool缓存池会按照itemview的itemtype(viewHolder类型)进行保存,每个viewHolder类型缓存个数为5个,超过就会被回收。

  1. 获取缓存流程(getViewFromPos()方法):

先从AttachedScrap中获取,通过pos匹配holder——>获取失败,从CacheView中获取,也是通过pos获取holder缓存——>获取失败,从自定义缓存中获取缓存——>获取失败,从mRecyclerPool中获取——>获取失败,重新创建viewholder——createViewHolder并bindview。

RecycleView相关面试题_第2张图片

(如果某级缓存获取成功后会对该级缓存进行删除,以免占用缓存)

RecycleView相关面试题_第3张图片

了解了缓存结构和缓存流程,我们再来看看具体的问题: 滑动10个item,再滑回去,会有几个执行onBindView?

参考文:https://www.cnblogs.com/jimuzz/p/14040674.html

由之前的缓存结构可知,需要重新执行onBindView的只有一种缓存区,就是缓存池mRecyclerPool。

所以我们假设从加载RecyclView开始盘的话(页面假设可以容纳7条数据):

  • 首先,7条数据会依次调用onCreateViewHolder和onBindViewHolder。

  • 往下滑一条(position=7),那么会把position=0的数据放到mCacheViews中。此时mCacheViews缓存区数量为1,mRecyclerPool数量为0。然后新出现的position=7的数据通过postion在mCacheViews中找不到对应的ViewHolder,通过itemtype也在mRecyclerPool中找不到对应的数据,所以会调用onCreateViewHolder和onBindViewHolder方法。

  • 再往下滑一条数据(position=8),如上。

  • 再往下滑一条数据(position=9),position=2的数据会放到mCacheViews中,但是由于mCacheViews缓存区默认容量为2,所以position=0的数据会被清空,然后放到mRecyclerPool缓存池中。而新出现的position=9数据由于在mRecyclerPool中还是找不到相应type的ViewHolder,所以还是会走onCreateViewHolder和onBindViewHolder方法。所以此时mCacheViews缓存区数量为2,mRecyclerPool数量为1。

  • 再往下滑一条数据(position=10),这时候由于可以在mRecyclerPool中找到相同viewtype的ViewHolder了。所以就直接复用了,并调用onBindViewHolder方法绑定数据。

  • 后面依次类推,刚消失的两条数据会被放到mCacheViews中,再出现的时候是不会调用onBindViewHolder方法,而复用的第三条数据是从mRecyclerPool中取得,就会调用onBindViewHolder方法了。

4.所以这个问题就得出结论了(假设mCacheViews容量为默认值2):

  • 如果一开始滑动的是新数据,那么滑动10个,就会走10个bindview方法。然后滑回去,会走10-2个bindview方法。一共18次调用。

  • 如果一开始滑动的是老数据,那么滑动10-2个,就会走8个bindview方法。然后滑回去,会走10-2个bindview方法。一共16次调用。

但是但是,实际情况又有点不一样。因为Recycleview在v25版本引入了一个新的机制,预取机制。

预取机制,就是在滑动过程中,会把将要展示的一个元素提前缓存到mCachedViews中,所以滑动10个元素的时候,第11个元素也会被创建,也就多走了一次bindview方法。但是滑回去的时候不影响,因为就算提前取了一个缓存数据,只是把bindview方法提前了,并不影响总的绑定item数量。

所以滑动的是新数据的情况下就会多一次调用bindview方法。

5总结,问题怎么答呢?

四级缓存和流程说一下。

滑动10个,再滑回去,bindview可以是19次调用,可以是16次调用。

缓存的其实就是缓存item的view,在Recycleview中就是viewholder。

cachedView就是mCacheViews缓存区中的view,是不需要重新绑定数据的。

其他问题:

为啥需要Scrap一级缓存?

主要用在插入或是删除itemView时,先把屏幕内的ViewHolder保存至AttachedScrap中,作用在LayoutManager中,它仅仅把需要从ViewGroup中移除的子view设置它的父view为null,从而实现了从RecyclerView中移除操作detachView()。不过如果是全部刷新(适配器的notifyDataSetChanged方法),那么不会走Scrap缓存;

RecycleView 滑动冲突解决:

除了可以参考之前我的文章里提到的两种常用解决方案: https://blog.csdn.net/emmmsuperdan/article/details/79645792

如果是双层滑动嵌套情况,可以优化成NestScrollView嵌套RecyclerView方案:

但是要注意设置RecyclerView.setNestedScrollingEnabled(false)这个方法,用来取消RecyclerView本身的滑动效果。

这是因为RecyclerView默认是setNestedScrollingEnabled(true),这个方法的含义是支持嵌套滚动的。也就是说当它嵌套在NestedScrollView中时,默认会随着NestedScrollView滚动而滚动,放弃了自己的滚动。

RecycleView绘制

可参考文:https://blog.csdn.net/qq_32019367/article/details/114656817

在开始onMeasure的地方,对Rv的宽高Mode做了判断,如果是固定宽和高(Exactly_Mode)或者设置了setHasFixedSize,就直接return开始执行下一步layout,为啥固定宽高不需要measure呢?因为固定宽高,一页子view的数量就确定了,直接做layout摆放就行;因此如果在使用rv时知道是固定宽高,可以提前指定或setHasFixedSize,这样更节省绘制性能;

如何实现RecyclerView的局部更新,用过payload吗,notifyItemChange方法中的参数?

关于RecyclerView的数据更新,主要有以下几个方法:

  • notifyDataSetChanged(),刷新全部可见的item。

  • notifyItemChanged(int),刷新指定item。

  • notifyItemRangeChanged(int,int),从指定位置开始刷新指定个item。

  • notifyItemInserted(int)、notifyItemMoved(int)、notifyItemRemoved(int)。插入、移动一个并自动刷新。

  • notifyItemChanged(int, Object),局部刷新。

可以看到,关于view的局部刷新就是notifyItemChanged(int, Object)方法,下面具体说说:

notifyItemChange有两个构造方法:

  • notifyItemChanged(int position, @Nullable Object payload)

  • notifyItemChanged(int position)

其中payload参数可以认为是你要刷新的一个标示,比如我有时候只想刷新itemView中的textview,有时候只想刷新imageview?又或者我只想某一个view的文字颜色进行高亮设置?那么我就可以通过payload参数来标示这个特殊的需求了。

具体怎么做呢?比如我调用了notifyItemChanged(14,"changeColor"),那么在onBindViewHolder回调方法中做下判断即可:

    @Override
    public void onBindViewHolder(ViewHolderholder, int position, List payloads) {
        if (payloads.isEmpty()) {
            // payloads为空,说明是更新整个ViewHolder
            onBindViewHolder(holder, position);
        } else {
            // payloads不为空,这只更新需要更新的View即可。
            String payload = payloads.get(0).toString();
            if ("changeColor".equals(payload)) {
                holder.textView.setTextColor("");
            }
        }
    } 
   

RecyclerView嵌套RecyclerView滑动冲突,NestScrollView嵌套RecyclerView。

1)RecyclerView嵌套RecyclerView的情况下,如果两者都要上下滑动,那么就会引起滑动冲突。默认情况下外层的RecyclerView可滑,内层不可滑。

之前说过解决滑动冲突的办法有两种:内部拦截法和外部拦截法

这里我提供一种内部拦截法,还有一些其他的办法大家可以自己思考下。

   holder.recyclerView.setOnTouchListener{ v, event ->when(event.action){//当按下操作的时候,就通知父view不要拦截,拿起操作就设置可以拦截,正常走父view的滑动。
                MotionEvent.ACTION_DOWN,MotionEvent.ACTION_MOVE -> v.parent.requestDisallowInterceptTouchEvent(true)
                MotionEvent.ACTION_UP -> v.parent.requestDisallowInterceptTouchEvent(false)}false}

2)关于ScrclerView的滑动冲突还是同样的解决办法,就是进行事件拦截。

还有一个办法就是用Nestedscrollview代替ScrollView,Nestedscrollview是官方为了解决滑动冲突问题而设计的新的View。它的定义就是支持嵌套滑动的ScrollView。

所以直接替换成Nestedscrollview就能保证两者都能正常滑动了。但是要注意设置RecyclerView.setNestedScrollingEnabled(false)这个方法,用来取消RecyclerView本身的滑动效果。

这是因为RecyclerView默认是setNestedScrollingEnabled(true),这个方法的含义是支持嵌套滚动的。也就是说当它嵌套在NestedScrollView中时,默认会随着NestedScrollView滚动而滚动,放弃了自己的滚动。所以给我们的感觉就是滞留、卡顿。所以我们将它设置为false就解决了卡顿问题,让他正常的滑动,不受外部影响。

说说RecyclerView性能优化。

  • bindViewHolder方法是在UI线程进行的,此方法不能耗时操作,不然将会影响滑动流畅性。比如进行日期的格式化。

  • 对于新增或删除的时候,可以使用diffutil进行局部刷新,少用全局刷新

  • 对于itemVIew进行布局优化,比如少嵌套等。

  • 25.1.0 (>=21)及以上使用Prefetch 功能,也就是预取功能,嵌套时且使用的是LinearLayoutManager,子RecyclerView可通过setInitialPrefatchItemCount设置预取个数

  • 加大RecyclerView缓存,比如cacheview大小默认为2,可以设置大点,用空间来换取时间,提高流畅度

  • 如果高度固定,可以设置setHasFixedSize(true)来避免requestLayout浪费资源,否则每次更新数据都会重新测量高度。

void onItemsInsertedOrRemoved() {
   if (hasFixedSize) layoutChildren();
   else requestLayout();
}
  • 如果多个RecycledView 的 Adapter 是一样的,比如嵌套的 RecyclerView 中存在一样的 Adapter,可以通过设置 RecyclerView.setRecycledViewPool(pool);来共用一个 RecycledViewPool。这样就减少了创建VIewholder的开销。

  • 在RecyclerView的元素比较高,一屏只能显示一个元素的时候,第一次滑动到第二个元素会卡顿。这种情况就可以通过设置额外的缓存空间,重写getExtraLayoutSpace方法即可。

new LinearLayoutManager(this) {
    @Override
    protected int getExtraLayoutSpace(RecyclerView.State state) {
        return size;
    }
};
  • 设置RecyclerView.addOnScrollListener();来在滑动过程中停止加载的操作。

  • 减少对象的创建,比如设置监听事件,可以全局创建一个,所有view公用一个listener,并且放到CreateView里面去创建监听,因为CreateView调用要少于bindview。这样就减少了对象创建所造成的消耗

  • 用notifyDataSetChange时,适配器不知道整个数据集中的那些内容以及存在,再重新匹配ViewHolder时会花生闪烁。设置adapter.setHasStableIds(true),并重写getItemId()来给每个Item一个唯一的ID,也就是唯一标识,就使itemview的焦点固定,解决了闪烁问题。

通过Demo再理解一遍

demo来自:https://www.jianshu.com/p/3e9aa4bdaefd

demo下载地址:https://github.com/kaka10xiaobang/RecyclerViewCacheDemo

简单说一下Demo里面需要注意的代码,下面是对RecyclerView的一个包装

public class RecyclerViewWrapper extends RecyclerView {
    private  LayoutListener layoutListener;

    public RecyclerViewWrapper(@NonNull Context context) {
        super(context);
    }

    public RecyclerViewWrapper(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public RecyclerViewWrapper(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    public void setLayoutListener(LayoutListener layoutListener) {
        this.layoutListener = layoutListener;
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (layoutListener != null) {
            layoutListener.onBeforeLayout();
        }
        super.onLayout(changed, l, t, r, b);

        if (layoutListener != null) {
            layoutListener.onAfterLayout();
        }
    }

    public interface LayoutListener {

        void onBeforeLayout();

        void onAfterLayout();
    }

}

其实很简单,在RecyclerView执行onLayout()方法前后执行一下咱们打印缓存变化的方法

再看一眼打印缓存变化的方法,利用反射的技术

/**
     * 利用java反射机制拿到RecyclerView内的缓存并打印出来
     * */
    private void showMessage(RecyclerViewWrapper rv) {
        try {
            Field mRecycler =
                    Class.forName("androidx.recyclerview.widget.RecyclerView").getDeclaredField("mRecycler");
            mRecycler.setAccessible(true);
            RecyclerView.Recycler recyclerInstance = (RecyclerView.Recycler) mRecycler.get(rv);

            Class recyclerClass = Class.forName(mRecycler.getType().getName());
            Field mViewCacheMax = recyclerClass.getDeclaredField("mViewCacheMax");
            Field mAttachedScrap = recyclerClass.getDeclaredField("mAttachedScrap");
            Field mChangedScrap = recyclerClass.getDeclaredField("mChangedScrap");
            Field mCachedViews = recyclerClass.getDeclaredField("mCachedViews");
            Field mRecyclerPool = recyclerClass.getDeclaredField("mRecyclerPool");
            mViewCacheMax.setAccessible(true);
            mAttachedScrap.setAccessible(true);
            mChangedScrap.setAccessible(true);
            mCachedViews.setAccessible(true);
            mRecyclerPool.setAccessible(true);


            int mViewCacheSize = (int) mViewCacheMax.get(recyclerInstance);
            ArrayListWrapper mAttached =
                    (ArrayListWrapper) mAttachedScrap.get(recyclerInstance);
            ArrayList mChanged =
                    (ArrayList) mChangedScrap.get(recyclerInstance);
            ArrayList mCached =
                    (ArrayList) mCachedViews.get(recyclerInstance);
            RecyclerView.RecycledViewPool recycledViewPool =
                    (RecyclerView.RecycledViewPool) mRecyclerPool.get(recyclerInstance);

            Class recyclerPoolClass = Class.forName(mRecyclerPool.getType().getName());

            Log.e(TAG, "mAttachedScrap(一缓) size is:" + mAttached.maxSize + ", \n" + "mCachedViews(二缓) max size is:" + mViewCacheSize + ","
                    + getMCachedViewsInfo(mCached) + getRVPoolInfo(recyclerPoolClass, recycledViewPool));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

注意:本文使用的RecyclerView的版本是androidx,在调onAttachedToWindow()方法的时候会进行版本判断,如果是5.0以及以上的系统(即大于等于21),GapWorker会把RecyclerView自己加入到GapWorker。在RenderThread线程执行预取操作的时候会mPrefetchMaxCountObserved = 1,这就会导致你使用5.0以及以上系统的手机打印缓存数量的时候会比你预想的多一个。这里为了不造成这种问题,本文使用4.4系统的Android模拟器来演示Demo。

Demo演示效果截图

  • 启动App,第一次加载的情况

RecycleView相关面试题_第4张图片

初始化加载只有屏幕内的一级缓存7个

  • 把position = 0 和position=1 两个item移除屏幕

RecycleView相关面试题_第5张图片

看蓝色框出来的,position = 0 和position = 1的item被加入到了Cache缓存中,Cache的缓存数量我没有修改,默认2个,也就说现在已经满了

  • 再把position = 2的item也移除屏幕

RecycleView相关面试题_第6张图片

因为上一步Cache里面的缓存已经慢了,此时position = 2又被加入缓存,根据FIFO的原则,cache里面position = 0 被remove掉并加入到了四级缓存RecycledView里面,此时RecycledView也有了缓存并且该缓存没有任何有效数据信息。

  • 再上一步的基础上下拉一下,把position = 2的item显示出来

RecycleView相关面试题_第7张图片

此时position = 2的item将要被显示出来,会先从cache里面找,发现Cache正好有position = 2的缓存就直接拿出来复用了,并且原来在屏幕里的position= 9 的item被移除了,就会加入到Cache的缓存里

-----------------------------------分割线-------------------------------------------

现在看一下onCreateViewHolder()和onBindViewHolder()的情况

  • 还是启动App,第一次加载后,再把position = 0和position =1的item移除屏幕再移回来

RecycleView相关面试题_第8张图片

onBindViewHolder()方法没有被重复执行(静态图显示的效果不是很好,gif录制的质量太差了,还是建议下载demo自己尝试一下)

  • 最后留一个问题给大家

RecycleView相关面试题_第9张图片

为什么在第10次onCreateViewHolder()执行以后就再也没有执行过onCreateViewHolder()方法了?

参考:RecyclerView中ViewHolder的复用回收

注意:在只有一种itemype的情况下,RecyclerPool里面只会有一条数据,即该itemype对应的list种只有一条数据。

023-03-22 01:38:56.749 11733-11733/com.kaka.recyclerview E/MainActivity: mAttachedScrap(一缓) size is:0, 
    mCachedViews(二缓) max size is:3,mCachedViews(二缓) info:  
     mCachedViews[0] is ViewHolder{ce28bea position=4 id=-1, oldPos=-1, pLpos:-1 no parent}
     mCachedViews[1] is ViewHolder{4130edb position=5 id=-1, oldPos=-1, pLpos:-1 no parent}
     mCachedViews[2] is ViewHolder{62a7d5 position=14 id=-1, oldPos=-1, pLpos:-1 no parent} 
    
     mRecyclerPool(四缓) info:  
2023-03-22 01:38:56.757 11733-11733/com.kaka.recyclerview E/MainActivity: mAttachedScrap(一缓) size is:0, 
    mCachedViews(二缓) max size is:3,mCachedViews(二缓) info:  
     mCachedViews[0] is ViewHolder{ce28bea position=4 id=-1, oldPos=-1, pLpos:-1 no parent}
     mCachedViews[1] is ViewHolder{4130edb position=5 id=-1, oldPos=-1, pLpos:-1 no parent}
     mCachedViews[2] is ViewHolder{62a7d5 position=14 id=-1, oldPos=-1, pLpos:-1 no parent} 
    
     mRecyclerPool(四缓) info:  
2023-03-22 01:38:56.765 11733-11733/com.kaka.recyclerview E/MainActivity: mAttachedScrap(一缓) size is:0, 
    mCachedViews(二缓) max size is:3,mCachedViews(二缓) info:  
     mCachedViews[0] is ViewHolder{ce28bea position=4 id=-1, oldPos=-1, pLpos:-1 no parent}
     mCachedViews[1] is ViewHolder{4130edb position=5 id=-1, oldPos=-1, pLpos:-1 no parent}
     mCachedViews[2] is ViewHolder{62a7d5 position=14 id=-1, oldPos=-1, pLpos:-1 no parent} 
    
     mRecyclerPool(四缓) info:  
2023-03-22 01:38:56.773 11733-11733/com.kaka.recyclerview E/MainActivity: mAttachedScrap(一缓) size is:0, 
    mCachedViews(二缓) max size is:3,mCachedViews(二缓) info:  
     mCachedViews[0] is ViewHolder{ce28bea position=4 id=-1, oldPos=-1, pLpos:-1 no parent}
     mCachedViews[1] is ViewHolder{4130edb position=5 id=-1, oldPos=-1, pLpos:-1 no parent}
     mCachedViews[2] is ViewHolder{62a7d5 position=14 id=-1, oldPos=-1, pLpos:-1 no parent} 
    
     mRecyclerPool(四缓) info:  
2023-03-22 01:38:56.782 11733-11733/com.kaka.recyclerview E/MainActivity: mAttachedScrap(一缓) size is:0, 
    mCachedViews(二缓) max size is:3,mCachedViews(二缓) info:  
     mCachedViews[0] is ViewHolder{ce28bea position=4 id=-1, oldPos=-1, pLpos:-1 no parent}
     mCachedViews[1] is ViewHolder{4130edb position=5 id=-1, oldPos=-1, pLpos:-1 no parent}
     mCachedViews[2] is ViewHolder{62a7d5 position=14 id=-1, oldPos=-1, pLpos:-1 no parent} 
    
     mRecyclerPool(四缓) info:  
2023-03-22 01:38:56.798 11733-11733/com.kaka.recyclerview E/MainActivity: mAttachedScrap(一缓) size is:0, 
    mCachedViews(二缓) max size is:3,mCachedViews(二缓) info:  
     mCachedViews[0] is ViewHolder{ce28bea position=4 id=-1, oldPos=-1, pLpos:-1 no parent}
     mCachedViews[1] is ViewHolder{4130edb position=5 id=-1, oldPos=-1, pLpos:-1 no parent}
     mCachedViews[2] is ViewHolder{62a7d5 position=14 id=-1, oldPos=-1, pLpos:-1 no parent} 
    
     mRecyclerPool(四缓) info:  
2023-03-22 01:38:56.806 11733-11733/com.kaka.recyclerview E/MainActivity: mAttachedScrap(一缓) size is:0, 
    mCachedViews(二缓) max size is:3,mCachedViews(二缓) info:  
     mCachedViews[0] is ViewHolder{ce28bea position=4 id=-1, oldPos=-1, pLpos:-1 no parent}
     mCachedViews[1] is ViewHolder{4130edb position=5 id=-1, oldPos=-1, pLpos:-1 no parent}
     mCachedViews[2] is ViewHolder{62a7d5 position=14 id=-1, oldPos=-1, pLpos:-1 no parent} 
    
     mRecyclerPool(四缓) info:  
2023-03-22 01:38:56.815 11733-11733/com.kaka.recyclerview E/MainActivity: mAttachedScrap(一缓) size is:0, 
    mCachedViews(二缓) max size is:3,mCachedViews(二缓) info:  
     mCachedViews[0] is ViewHolder{4130edb position=5 id=-1, oldPos=-1, pLpos:-1 no parent}
     mCachedViews[1] is ViewHolder{3063d78 position=6 id=-1, oldPos=-1, pLpos:-1 no parent}
     mCachedViews[2] is ViewHolder{62a7d5 position=14 id=-1, oldPos=-1, pLpos:-1 no parent} 
    
     mRecyclerPool(四缓) info:  >>> mScrap[0] 中的 mScrapHeap[0] info is:ViewHolder{ce28bea position=-1 id=-1, oldPos=-1, pLpos:-1 unbound no parent}
2023-03-22 01:38:56.832 11733-11733/com.kaka.recyclerview E/MainActivity: mAttachedScrap(一缓) size is:0, 
    mCachedViews(二缓) max size is:3,mCachedViews(二缓) info:  
     mCachedViews[0] is ViewHolder{4130edb position=5 id=-1, oldPos=-1, pLpos:-1 no parent}
     mCachedViews[1] is ViewHolder{3063d78 position=6 id=-1, oldPos=-1, pLpos:-1 no parent}
     mCachedViews[2] is ViewHolder{62a7d5 position=14 id=-1, oldPos=-1, pLpos:-1 no parent} 
    
     mRecyclerPool(四缓) info:  >>> mScrap[0] 中的 mScrapHeap[0] info is:ViewHolder{ce28bea position=-1 id=-1, oldPos=-1, pLpos:-1 unbound no parent}
2023-03-22 01:38:56.848 11733-11733/com.kaka.recyclerview E/MainActivity: mAttachedScrap(一缓) size is:0, 
    mCachedViews(二缓) max size is:3,mCachedViews(二缓) info:  
     mCachedViews[0] is ViewHolder{4130edb position=5 id=-1, oldPos=-1, pLpos:-1 no parent}
     mCachedViews[1] is ViewHolder{3063d78 position=6 id=-1, oldPos=-1, pLpos:-1 no parent}
     mCachedViews[2] is ViewHolder{62a7d5 position=14 id=-1, oldPos=-1, pLpos:-1 no parent} 
    
     mRecyclerPool(四缓) info:  >>> mScrap[0] 中的 mScrapHeap[0] info is:ViewHolder{ce28bea position=-1 id=-1, oldPos=-1, pLpos:-1 unbound no parent}
2023-03-22 01:38:56.881 11733-11733/com.kaka.recyclerview E/MainActivity: mAttachedScrap(一缓) size is:0, 
    mCachedViews(二缓) max size is:3,mCachedViews(二缓) info:  
     mCachedViews[0] is ViewHolder{4130edb position=5 id=-1, oldPos=-1, pLpos:-1 no parent}
     mCachedViews[1] is ViewHolder{3063d78 position=6 id=-1, oldPos=-1, pLpos:-1 no parent}
     mCachedViews[2] is ViewHolder{62a7d5 position=14 id=-1, oldPos=-1, pLpos:-1 no parent} 
    
     mRecyclerPool(四缓) info:  >>> mScrap[0] 中的 mScrapHeap[0] info is:ViewHolder{ce28bea position=-1 id=-1, oldPos=-1, pLpos:-1 unbound no parent}

从这里的log中可以看出mRecyclerPool里面的数据是从mCachedViews里面移除来的,且只有一条数据。

扩展:

Android从源码分析RecyclerView四级缓存复用机制(缓存ViewHolder)


RecyclerView中ViewHolder的复用回收

前言

本文是本人学习RecyclerView的ViewHolder回收复用的过程记录,仅为自己复习时使用。

大家在使用RecyclerView时都会创建自己的Adapter并重写相关的方法。

class MyAdapter : RecyclerView.Adapter() {
    //创建ViewHolder
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {

    }

    //ViewHolder绑定数据
    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {

    }

    //数据个数
    override fun getItemCount(): Int {

    }
}

这里有一个疑问何时为什么有时候onCreateViewHolder和onBindViewHolder都会调用,有时候只会调用onBindViewHolder呢。看完ViewHolder的复用后你可能会有自己的答案。

ViewHolder的复用

首先从RecyclerView的onTouchEvent说起吧。PS:下面的说明只是说明ViewHolder的调用路径。且该路径不唯一,后面会进行说明。

@Override
    public boolean onTouchEvent(MotionEvent e) {
        

        final int action = e.getActionMasked();
        final int actionIndex = e.getActionIndex();

        switch (action) {
            case MotionEvent.ACTION_DOWN: {
              ......
            } break;

            case MotionEvent.ACTION_POINTER_DOWN: {
              ......
            } break;

            case MotionEvent.ACTION_MOVE: {
                .....
                //调用scrollByInternal方法
                    if (scrollByInternal(
                            canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0,
                            e)) {
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
                    if (mGapWorker != null && (dx != 0 || dy != 0)) {
                        mGapWorker.postFromTraversal(this, dx, dy);
                    }
                }
            } break;

        .......

        return true;
    }

在onTouchEvent方法中调用scrollByInternal方法。

 boolean scrollByInternal(int x, int y, MotionEvent ev) {
        int unconsumedX = 0;
        int unconsumedY = 0;
        int consumedX = 0;
        int consumedY = 0;

        consumePendingUpdateOperations();
        if (mAdapter != null) {
            mReusableIntPair[0] = 0;
            mReusableIntPair[1] = 0;
            scrollStep(x, y, mReusableIntPair);
            consumedX = mReusableIntPair[0];
            consumedY = mReusableIntPair[1];
            unconsumedX = x - consumedX;
            unconsumedY = y - consumedY;
        }
        if (!mItemDecorations.isEmpty()) {
            invalidate();
        }

        mReusableIntPair[0] = 0;
        mReusableIntPair[1] = 0;
        dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
                TYPE_TOUCH, mReusableIntPair);
        unconsumedX -= mReusableIntPair[0];
        unconsumedY -= mReusableIntPair[1];
        boolean consumedNestedScroll = mReusableIntPair[0] != 0 || mReusableIntPair[1] != 0;

        // Update the last touch co-ords, taking any scroll offset into account
        mLastTouchX -= mScrollOffset[0];
        mLastTouchY -= mScrollOffset[1];
        mNestedOffsets[0] += mScrollOffset[0];
        mNestedOffsets[1] += mScrollOffset[1];

        if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
            if (ev != null && !MotionEventCompat.isFromSource(ev, InputDevice.SOURCE_MOUSE)) {
                pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY);
            }
            considerReleasingGlowsOnScroll(x, y);
        }
        if (consumedX != 0 || consumedY != 0) {
            dispatchOnScrolled(consumedX, consumedY);
        }
        if (!awakenScrollBars()) {
            invalidate();
        }
        return consumedNestedScroll || consumedX != 0 || consumedY != 0;
    }

scrollByInternal中又调用了scrollStep方法

 boolean scrollByInternal(int x, int y, MotionEvent ev) {
        int unconsumedX = 0;
        int unconsumedY = 0;
        int consumedX = 0;
        int consumedY = 0;

        consumePendingUpdateOperations();
        if (mAdapter != null) {
            mReusableIntPair[0] = 0;
            mReusableIntPair[1] = 0;
           //调用scrollStep方法
            scrollStep(x, y, mReusableIntPair);
            consumedX = mReusableIntPair[0];
            consumedY = mReusableIntPair[1];
            unconsumedX = x - consumedX;
            unconsumedY = y - consumedY;
        }
        .......
        return consumedNestedScroll || consumedX != 0 || consumedY != 0;
    }

scrollStep中调用了LayoutManager的scrollVerticallyBy方法(这里仅分析LinearLayoutManager的scrollVerticallyBy)。

  void scrollStep(int dx, int dy, @Nullable int[] consumed) {
        startInterceptRequestLayout();
        onEnterLayoutOrScroll();

        TraceCompat.beginSection(TRACE_SCROLL_TAG);
        fillRemainingScrollValues(mState);

        int consumedX = 0;
        int consumedY = 0;  
        //调用到scrollHorizontallyBy方法,该方法是LayoutManager的方法
        if (dx != 0) {
            consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
        }
        if (dy != 0) {
            consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
        }

       
    }

LayoutManager的scrollVerticallyBy方法调用了scrollBy方法

   public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
            RecyclerView.State state) {
        if (mOrientation == HORIZONTAL) {
            return 0;
        }
       //调用scrollBy方法
        return scrollBy(dy, recycler, state);
    }

scrollBy中调用了fill方法。

int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (getChildCount() == 0 || delta == 0) {
            return 0;
        }
        ensureLayoutState();
        mLayoutState.mRecycle = true;
        final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
        final int absDelta = Math.abs(delta);
        updateLayoutState(layoutDirection, absDelta, true, state);
       //调用LayoutState的fill方法
        final int consumed = mLayoutState.mScrollingOffset
                + fill(recycler, mLayoutState, state, false);
       ...
        return scrolled;
    }

fill方法调用了layoutChunk方法

 int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
       ...
        int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
        LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            layoutChunkResult.resetInternal();
            if (RecyclerView.VERBOSE_TRACING) {
                TraceCompat.beginSection("LLM LayoutChunk");
            }
           //调用layoutChunk方法
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
           ....
        return start - layoutState.mAvailable;
    }

layoutChunk调用了LayoutState的next方法

  void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
        //调用了next方法
        View view = layoutState.next(recycler);
       ...
//添加获取到的View
 if (layoutState.mScrapList == null) {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addView(view);
            } else {
                addView(view, 0);
            }
        } else {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addDisappearingView(view);
            } else {
                addDisappearingView(view, 0);
            }
        }
    }

LayoutState的next方法中调用了Recycler的getViewForPosition方法

  View next(RecyclerView.Recycler recycler) {
//调用Recycler的getViewForPosition方法
        final View view = 
recycler.getViewForPosition(mCurrentPosition);
        mCurrentPosition += mItemDirection;
        return view;
    }

getViewForPosition方法最终会调用到tryGetViewHolderForPositionByDeadline这个方法,而这个方法说明了RecyclerView中的ViewHolder是如何复用的。再看这个方法之前,让我们先来说一下RecyclerView的分级缓存吧。

public View getViewForPosition(int position) {
            //getViewForPosition调用重载方法
            return getViewForPosition(position, false);
        }
 View getViewForPosition(int position, boolean dryRun) {
            return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
        }

RecyclerView的缓存分为四级:

RecycleView相关面试题_第10张图片

scrap

rv之所以要将缓存分成这么多块肯定在功能上是有一定的区分的,它们分别对应不同的使用场景,scrap是用来保存被rv移除掉但最近又马上要使用的缓存,比如说rv中自带item的动画效果。

本质上就是计算item的偏移量然后执行属性动画的过程,这中间可能就涉及到需要将动画之前的item保存下位置信息,动画后的item再保存下位置信息,然后利用这些位置数据生成相应的属性动画。如何保存这些viewholer呢,就需要使用到scrap了,因为这些viewholer数据上是没有改变的,只是位置改变而已,所以放置到scrap最为合适。

稍微仔细看的话就能发现scrap缓存有两个成员mChangedScrap和mAttachedScrap,它们保存的对象有些不一样,一般调用adapter的notifyItemRangeChanged被移除的viewholder会保存到mChangedScrap,其余的notify系列方法(不包括notifyDataSetChanged)移除的viewholder会被保存到mAttachedScrap中。

简单来说就是执行Notify动画时,notifyItemRangeChanged移除的ViewHolder会放入mChangedScrap,其余动画(不包括notifyDataSetChanged)移除的ViewHolder会保存到mAttachedScrap

cached

也是rv中非常重要的一个缓存,就linearlayoutmanager来说cached缓存默认大小为2,它的容量非常小,所起到的作用就是rv滑动时刚被移出屏幕的viewholer的收容所。

因为rv会认为刚被移出屏幕的viewholder可能接下来马上就会使用到,所以不会立即设置为无效viewholer,会将它们保存到cached中,但又不能将所有移除屏幕的viewholder都视为有效viewholer,所以它的默认容量只有2个,当然我们可以通过setViewCacheSize(intviewCount)来改变默认的大小

extension

第三级缓存,这是一个自定义的缓存,没错rv是可以自定义缓存行为的,在这里你可以决定缓存的保存逻辑,但是这么个自定义缓存一般都没有见过具体的使用场景,而且自定义缓存需要你对rv中的源码非常熟悉才行,否则在rv执行item动画,或者执行notify的一系列方法后你的自定义缓存是否还能有效就是一个值得考虑的问题。

所以一般不太推荐使用该缓存,更多的我觉得这可能是google自已留着方便扩展来使用的,目前来说这还只是个空实现而已,从这点来看其实rv所说的四级缓存本质上还只是三级缓存。

Pool

又一个重要的缓存,这也是唯一一个我们开发者可以方便设置的一个(虽然extension也能设置,但是难度大),而且设置方式非常简单,new一个pool传进去就可以了,其他的都不用我们来处理,google已经给我们料理好后事了,这个缓存保存的对象就是那些无效的viewholer,虽说无效的viewholer上的数据是无效的,但是它的rootview还是可以拿来使用的,这也是为什么最早的listview有一个convertView参数的原因,当然这种机制也被rv很好的继承了下来。

pool一般会和cached配合使用,这么来说,cached存不下的会被保存到pool中毕竟cached默认容量大小只有2,但是pool容量也是有限的当保存满之后再有viewholder到来的话就只能会无情抛弃掉,它也有一个默认的容量大小

privatestaticfinalintDEFAULT_MAX_SCRAP = 5;

intmMaxScrap = DEFAULT_MAX_SCRAP;

这个大小也是可以通过调用方法来改变,具体看应用场景,一般来说正常使用的话使用默认大小即可。

有了相关的知识我们再来看tryGetViewHolderForPositionByDeadline的代码。

ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
         
            ViewHolder holder = null;
         
            if (mState.isPreLayout()) {
                1.从Recycler的mChangedScrap中寻找ViewHolder
                holder = getChangedScrapViewForPosition(position);
                fromScrapOrHiddenOrCache = holder != null;
            }
  
            if (holder == null) {
               2.如果1中没有找到相关的ViewHolder,从Recycler的mAttachedScrap和mCachedViews中获取ViewHolder
                holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
                if (holder != null) {
                    if (!validateViewHolderForOffsetPosition(holder)) {
                        // recycle holder (and unscrap if relevant) since it can't be used
                        if (!dryRun) {
                            // we would like to recycle this but need to make sure it is not used by
                            // animation logic etc.
                            holder.addFlags(ViewHolder.FLAG_INVALID);
                            if (holder.isScrap()) {
                                removeDetachedView(holder.itemView, false);
                                holder.unScrap();
                            } else if (holder.wasReturnedFromScrap()) {
                                holder.clearReturnedFromScrapFlag();
                            }
                            recycleViewHolderInternal(holder);
                        }
                        holder = null;
                    } else {
                        fromScrapOrHiddenOrCache = true;
                    }
                }
            }
            if (holder == null) {
                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
                    throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
                            + "position " + position + "(offset:" + offsetPosition + ")."
                            + "state:" + mState.getItemCount() + exceptionLabel());
                }
                //获取type类型
                final int type = mAdapter.getItemViewType(offsetPosition);
                //3 2中没有找到相关的ViewHolder根据stableID和type再从Recycler的mAttachedScrap和mCachedViews中获取ViewHolder
                if (mAdapter.hasStableIds()) {
                    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                            type, dryRun);
                    if (holder != null) {
                        // update position
                        holder.mPosition = offsetPosition;
                        fromScrapOrHiddenOrCache = true;
                    }
                }
                if (holder == null && mViewCacheExtension != null) {
                    //4 3中没有找到查找自定义的缓存(需自己实现)
                    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
                    if (DEBUG) {
                        Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
                                + position + ") fetching from shared pool");
                    }
                    //5 4中也没有找到相关的ViewHolder,到pool中寻找
                    holder = getRecycledViewPool().getRecycledView(type);
                    if (holder != null) {
                        //重置ViewHolder的相关状态
                        holder.resetInternal();
                        if (FORCE_INVALIDATE_DISPLAY_LIST) {
                            invalidateDisplayListInt(holder);
                        }
                    }
                }
                //6 从5中也没有获取到ViewHolder
                if (holder == null) {
                    long start = getNanoTime();
                    if (deadlineNs != FOREVER_NS
                            && !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
                        // abort - we have a deadline we can't meet
                        return null;
                    }
                    // 7执行adaper的mAdapter.createViewHolder去创建相应的ViewHolder,
                    //该方法最终会调用到我们重写的onCreateViewHolder
                    holder = mAdapter.createViewHolder(RecyclerView.this, type);
                    if (ALLOW_THREAD_GAP_WORK) {
                        // only bother finding nested RV if prefetching
                        RecyclerView innerView = findNestedRecyclerView(holder.itemView);
                        if (innerView != null) {
                            holder.mNestedRecyclerView = new WeakReference<>(innerView);
                        }
                    }

                    long end = getNanoTime();
                    mRecyclerPool.factorInCreateTime(type, end - start);
                    if (DEBUG) {
                        Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder");
                    }
                }
            }

           ......

            boolean bound = false;
            if (mState.isPreLayout() && holder.isBound()) {
                // do not update unless we absolutely have to.
                holder.mPreLayoutPosition = position;
            } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
                if (DEBUG && holder.isRemoved()) {
                    throw new IllegalStateException("Removed holder should be bound and it should"
                            + " come here only in pre-layout. Holder: " + holder
                            + exceptionLabel());
                }
                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                //8  tryBindViewHolderByDeadline会调用adapter的bindViewHolder,最终会调用我们重写的onBindViewHolder方法执行数据的绑定
                bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
            }

            ....
            return holder;
        }

现在看一下各个方法吧

ViewHolder getChangedScrapViewForPosition(int position)

 ViewHolder getChangedScrapViewForPosition(int position) {
            // If pre-layout, check the changed scrap for an exact match.
            final int changedScrapSize;
            if (mChangedScrap == null || (changedScrapSize = mChangedScrap.size()) == 0) {
                return null;
            }
            // 通过position在mChangedScrap找寻对应的ViewHolder
            for (int i = 0; i < changedScrapSize; i++) {
                final ViewHolder holder = mChangedScrap.get(i);
                if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position) {
                    holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
                    return holder;
                }
            }
            // 没有找到再通过id找一篇
            if (mAdapter.hasStableIds()) {
                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                if (offsetPosition > 0 && offsetPosition < mAdapter.getItemCount()) {
                    final long id = mAdapter.getItemId(offsetPosition);
                    for (int i = 0; i < changedScrapSize; i++) {
                        final ViewHolder holder = mChangedScrap.get(i);
                        if (!holder.wasReturnedFromScrap() && holder.getItemId() == id) {
                            holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
                            return holder;
                        }
                    }
                }
            }
            return null;
        }

getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun)

 ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
            final int scrapCount = mAttachedScrap.size();

            // 从mAttachedScrap中找
            for (int i = 0; i < scrapCount; i++) {
                final ViewHolder holder = mAttachedScrap.get(i);
                if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
                        && !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
                    holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
                    return holder;
                }
            }

            if (!dryRun) {
                View view = mChildHelper.findHiddenNonRemovedView(position);
                if (view != null) {
                    // This View is good to be used. We just need to unhide, detach and move to the
                    // scrap list.
                    final ViewHolder vh = getChildViewHolderInt(view);
                    mChildHelper.unhide(view);
                    int layoutIndex = mChildHelper.indexOfChild(view);
                    if (layoutIndex == RecyclerView.NO_POSITION) {
                        throw new IllegalStateException("layout index should not be -1 after "
                                + "unhiding a view:" + vh + exceptionLabel());
                    }
                    mChildHelper.detachViewFromParent(layoutIndex);
                    scrapView(view);
                    vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP
                            | ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
                    return vh;
                }
            }

            // 找不到从mCachedViews中找
            final int cacheSize = mCachedViews.size();
            for (int i = 0; i < cacheSize; i++) {
                final ViewHolder holder = mCachedViews.get(i);
                // invalid view holders may be in cache if adapter has stable ids as they can be
                // retrieved via getScrapOrCachedViewForId
                if (!holder.isInvalid() && holder.getLayoutPosition() == position
                        && !holder.isAttachedToTransitionOverlay()) {
                    if (!dryRun) {
                        mCachedViews.remove(i);
                    }
                    if (DEBUG) {
                        Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position
                                + ") found match in cache: " + holder);
                    }
                    return holder;
                }
            }
            return null;
        }

getScrapOrCachedViewForId(long id, int type, boolean dryRun)

/**
根据id和viewType再从mAttachedScrap和mCachedViews中找一遍
**/ 
ViewHolder getScrapOrCachedViewForId(long id, int type, boolean dryRun) {
            // Look in our attached views first
            final int count = mAttachedScrap.size();
            //从mAttachedScrap找
            for (int i = count - 1; i >= 0; i--) {
                final ViewHolder holder = mAttachedScrap.get(i);
                if (holder.getItemId() == id && !holder.wasReturnedFromScrap()) {
                    if (type == holder.getItemViewType()) {
                        holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
                        if (holder.isRemoved()) {
                            // this might be valid in two cases:
                            // > item is removed but we are in pre-layout pass
                            // >> do nothing. return as is. make sure we don't rebind
                            // > item is removed then added to another position and we are in
                            // post layout.
                            // >> remove removed and invalid flags, add update flag to rebind
                            // because item was invisible to us and we don't know what happened in
                            // between.
                            if (!mState.isPreLayout()) {
                                holder.setFlags(ViewHolder.FLAG_UPDATE, ViewHolder.FLAG_UPDATE
                                        | ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED);
                            }
                        }
                        return holder;
                    } else if (!dryRun) {
                        // if we are running animations, it is actually better to keep it in scrap
                        // but this would force layout manager to lay it out which would be bad.
                        // Recycle this scrap. Type mismatch.
                        mAttachedScrap.remove(i);
                        removeDetachedView(holder.itemView, false);
                        quickRecycleScrapView(holder.itemView);
                    }
                }
            }

            // 从mCachedViews中找
            final int cacheSize = mCachedViews.size();
            for (int i = cacheSize - 1; i >= 0; i--) {
                final ViewHolder holder = mCachedViews.get(i);
                if (holder.getItemId() == id && !holder.isAttachedToTransitionOverlay()) {
                    if (type == holder.getItemViewType()) {
                        if (!dryRun) {
                            mCachedViews.remove(i);
                        }
                        return holder;
                    } else if (!dryRun) {
                        recycleCachedViewAt(i);
                        return null;
                    }
                }
            }
            return null;
        }

getViewForPositionAndType(this, position, type),自定义缓存需要自己实现(不精通RecyclerView源码还是洗洗睡了吧)

 public abstract View getViewForPositionAndType(@NonNull Recycler recycler, int position,int type);

总结一下,RecyclerView依次从mChangedScrap、mAttachedScrap、mCachedViews、mViewCacheExtension、mRecyclerPool中获取相应的ViewHolder,获取不到就自己创建ViewHolder,最后绑定数据到相应的ViewHolder。

这里需要注意的有一下几点:

1.mRecyclerPool中缓存的ViewHolder只有类型没有相关的数据,需要进行数据绑定

2.ViewHolder的复用导致自定义adapter中的onBindViewHolder和onCreateViewHolder调用次数不一致。

3.ViewHolder是View的一层疯转

方法调用链

入口:RecyclerView滑动 Move 事件 -->RecyclerView. scrollByInternal --> RecyclerView.scrollStep --> LayoutManager.scrollVerticallyBy

-->LayoutManager.scrollBy --> LayoutManager.fill --> LayoutManager.LayoutState.layoutChunk -->LayoutManager.LayoutState.next -->LayoutManager. addView(view);

layoutState.next --> Recycler.getViewForPosition --> Recycler.tryGetViewHolderForPositionByDeadline -->

怎么从集合中去获取:tryGetViewHolderForPositionByDeadline,分几种情况去获取ViewHolder

  1. getChangedScrapViewForPosition -- mChangeScrap 与notfitupdate动画相关

  1. getScrapOrHiddenOrCachedHolderForPosition -- mAttachedScrap 、mCachedViews

  1. getScrapOrCachedViewForId -- mAttachedScrap 、mCachedViews (ViewType,itemid)

  1. mViewCacheExtension.getViewForPositionAndType -- 自定义缓存

  1. getRecycledViewPool().getRecycledView -- 从缓冲池里面获取

除了从onTouchEvent这一条调用路境外,还有一条调用路径

RecyclerView.onLayout-->RecyclerView.dispatchLayout-->RecyclerView.dispatchLayoutStep2-->LayoutManager.onLayoutChildren->LayoutManager.fill->后面与前面的一致。

不管进入的调用链如何,最后都调用到了Recycler.tryGetViewHolderForPositionByDeadLine方法,根据四级缓存来获取活创建ViewHolder

ViewHolder的回收

复用的ViewHolder来自于四级缓存缓存的ViewHolder,那么四级缓存缓存的ViewHolder来自于哪里呢?看看ViewHolder的回收吧。

既然复用是从onTouchEvent进行分析的,这次我们从onLayout开始分析。

onLayout方法调用dispatchLayout方法

 protected void onLayout(boolean changed, int l, int t, int r, int b) {
        TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
         //调用dispatchLayout
        dispatchLayout();
        TraceCompat.endSection();
        mFirstLayoutComplete = true;
    }

dispatchLayout调用dispatchLayoutStep2方法

void dispatchLayout() {
        ....
        if (mState.mLayoutStep == State.STEP_START) {
            dispatchLayoutStep1();
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
                || mLayout.getHeight() != getHeight()) {
            // First 2 steps are done in onMeasure but looks like we have to run again due to
            // changed size.
            mLayout.setExactMeasureSpecsFrom(this);
            //调用dispatchLayoutStep2
            dispatchLayoutStep2();
        } else {
            // always make sure we sync them (to ensure mode is exact)
            mLayout.setExactMeasureSpecsFrom(this);
        }
        dispatchLayoutStep3();
    }

dispatchLayoutStep2调用LayoutManager的onLayoutChildren,这里分析LinearLayoutManager的onLayoutChildren方法

private void dispatchLayoutStep2() {
        startInterceptRequestLayout();
        onEnterLayoutOrScroll();
        mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
        mAdapterHelper.consumeUpdatesInOnePass();
        mState.mItemCount = mAdapter.getItemCount();
        mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;

        // Step 2: Run layout
        mState.mInPreLayout = false;
        //调用onLayoutChildren方法
        mLayout.onLayoutChildren(mRecycler, mState);

        mState.mStructureChanged = false;
        mPendingSavedState = null;

        // onLayoutChildren may have caused client code to disable item animations; re-check
        mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
        mState.mLayoutStep = State.STEP_ANIMATIONS;
        onExitLayoutOrScroll();
        stopInterceptRequestLayout(false);
    }

onLayoutChildren调用detachAndScrapAttachedViews方法

 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        ...
         //调用detachAndScrapAttachedViews
        detachAndScrapAttachedViews(recycler);
       ...
    }

detachAndScrapAttachedViews调用scrapOrRecycleView

 public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
            final int childCount = getChildCount();
            for (int i = childCount - 1; i >= 0; i--) {
                final View v = getChildAt(i);
                //调用scrapOrRecycleView
                scrapOrRecycleView(recycler, i, v);
            }
        }

scrapOrRecycleView是ViewHolder回收的核心方法

private void scrapOrRecycleView(Recycler recycler, int index, View view) {
            final ViewHolder viewHolder = getChildViewHolderInt(view);
            if (viewHolder.shouldIgnore()) {
                if (DEBUG) {
                    Log.d(TAG, "ignoring view " + viewHolder);
                }
                return;
            }
            //如果ViewHolder没有发生过改变
            if (viewHolder.isInvalid() && !viewHolder.isRemoved()
                    && !mRecyclerView.mAdapter.hasStableIds()) {
                removeViewAt(index);
                //缓存进mCacheViews或者mRecyclerPool中
                recycler.recycleViewHolderInternal(viewHolder);
            } else {
                //发生过改变(动画引起的改变)
                detachViewAt(index);
                //缓存进mChangedScrap或mAttachedScrap中
                recycler.scrapView(view);
                mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
            }
        }

ViewHolder如果发生过改变(由notify动画引起的)则会进入

scrapView方法进行缓存否则进入recycleViewHolderInternal方法进行缓存。现在我们先看recycleViewHolderInternal方法是如何缓存数据的

void recycleViewHolderInternal(ViewHolder holder) {
            .....
            if (forceRecycle || holder.isRecyclable()) {
               //如果mViewCacheMax (mCacheViews的最大长度,默认为2)大于0且ViewHolder没发生过变化
                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();
                     //mCachedViews长度大于等于最大长度且mCachedViews中有数据
                    if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                         //将第一个元素放入mRecyclerPool中
                        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;
                    }
                    //将ViewHolder放入mCahceViews的适当位置
                    mCachedViews.add(targetCacheIndex, holder);
                    cached = true;
                }
                //如果没有放入mCacheViews,则加入mRecyclerPool中
                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.mOwnerRecyclerView = null;
            }
        }

再看一下addViewHolderToRecycledViewPool这个方法

voidrecycleCachedViewAt(int cachedViewIndex){.....//添加viewHolder到mRecyclerPool中addViewHolderToRecycledViewPool(viewHolder,true);//移除对应位置的ViewHolder
            mCachedViews.remove(cachedViewIndex);}

再看一下addViewHolderToRecycledViewPool这个方法

void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {

....

//ViewHolder放入mRecyclerPool

getRecycledViewPool().putRecycledView(holder);

}

最后看一下putRecycledView这个方法

publicvoidputRecycledView(ViewHolder scrap){
            final int viewType = scrap.getItemViewType();//根据type获取对应的集合
            final ArrayList scrapHeap =getScrapDataForType(viewType).mScrapHeap;//如果对应集合的长度大于等于最大长度(默认为5),则直接丢弃该ViewHolder,不进行保存if(mScrap.get(viewType).mMaxScrap <= scrapHeap.size()){return;}if(DEBUG && scrapHeap.contains(scrap)){thrownewIllegalArgumentException("this scrap item already exists");}//重置该ViewHolder相关状态
            scrap.resetInternal();//将该ViewHolder加入集合
            scrapHeap.add(scrap);}

现在我们总结一下recycleViewHolderInternal都做了那些工作

1.判断mCacheViews是否大于默认的最大大小(默认为2),如果大于则将第一个ViewHolder放入mRecyclerPool中,再讲待回收的ViewHolder放入

mCacheViews的适当位置,如果待回收ViewHoldr没有放入mCacheViews,则放入mRecyclerPool。mRecyclerPool在添加时如果超过了对应viewType的默认集合大小(默认为5),则直接丢弃该ViewHolder,否则重置该ViewHolder的状态并放入对应viewType的集合中。

需要注意的点:

1.mRecyclerPool中的ViewHolder来自于mCacheViews

2.mCacheViews是一个list充当的队列

3.mRecyclerPool中的ViewHolder都是不带数据的

分析完recycleViewHolderInternal我们再看一下scrapView方法

void scrapView(View view) {
            //找到对应的ViewHolder
            final ViewHolder holder = getChildViewHolderInt(view);
             //如果是notify的update动画
            if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
                    || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
                if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {
                    throw new IllegalArgumentException("Called scrap view with an invalid view."
                            + " Invalid views cannot be reused from scrap, they should rebound from"
                            + " recycler pool." + exceptionLabel());
                }
                holder.setScrapContainer(this, false);
                //将viewHolder添加入mAttachedScrap
                mAttachedScrap.add(holder);
            } else {
                if (mChangedScrap == null) {
                    mChangedScrap = new ArrayList();
                }
                //除了update动画之外,将viewHolder添加入mChangedScrap
                holder.setScrapContainer(this, true);
                mChangedScrap.add(holder);
            }
        }

scrapView只要执行了将ViewHolder是否是notify的update动画,如果是则加入mChangedScrap,否则加入mAttachedScrap中。

分析回收方法的方法调用链

LinearLayoutManager.onLayoutChildren --> detachAndScrapAttachedViews --> scrapOrRecycleView

--> 1.recycler.recycleViewHolderInternal(viewHolder); -- 处理 CacheView 、RecyclerViewPool 的缓存

--> 1.ViewHodler改变 不会进来 -- 先判断mCachedViews的大小

    --> mCachedViews.size 大于默认大小  --- recycleCachedViewAt 
    --- >addViewHolderToRecycledViewPool --- 缓存池里面的数据都是从mCachedViews里面出来的

--> 2.addViewHolderToRecycledViewPool --> getRecycledViewPool().putRecycledView(holder);

    --> scrap.resetInternal();  ViewHolder 清空

--> 2.recycler.scrapView(view);

还有一个回收的方法调用链调用是在onTouchEvent开始:

RecyclerView.onTouchEvent(move事件)-->RecyclerView.scrollByInternal->RecyclerView.scrollStep-->LayoutManager.mLayout.scrollVerticallyBy(仅分析竖直方向的滑动,以LinearLayoutManager.scrollVerticallyBy为例)-->LayoutManager.scrollBy-->LayoutManager.fill-->LayoutManager.recycleByLayoutState-->LayoutManager.recycleByLayoutState-->LayoutManager.recycleViewsFromStart-->LayoutManager.recycleChildren-->LayoutManager.removeAndRecycleViewAt-->Recycler.recycleView-->recycler.recycleViewHolderInternal

这里需要注意的一点,在onTouchEvent这条线中,并没有调用scapView这个方法,原因是scapView这个方法是判断回收的ViewHolder是否发生过改变,而ViewHolder发生改变是通过notify的动画来实现的,而notify动画的改变会引起RecyclerView的重新布局(重新调用onLayout方法,所以onLayout的那条调用链上存在scapView这个方法),而滑动不会引起动画,所以不需要scapView方法,仅调用recycleViewHolderInternal即可

总结

touchEvent和onLayout都会执行ViewHolder的回收复用,所不同的是onLayout的回收要考虑Item动画的,而onTouchEvent的回收不需要考虑动画的,因为执行动画会重新调用onLayout,所以在onLayout时会考虑item动画移除的ViewHolder。

你可能感兴趣的:(缓存)