仿探探划卡片 -- RecyclerView的四级缓存

2,000 阅读6分钟

前情提要

总所周知,面试老爱问RecyclerView的四级缓存,但是到底啥是四级缓存呢?

缓存和复用到底是啥?

说白了所谓的缓存,就是将已经加载过的对象存到集合里(List、Map),然后下回要用的时候再去集合里取。
例如:RecyclerView将上滑,滑出屏幕的itemView,存到一个列表里。然后下面加载的时候,就直接从列表里取,不去重新create
也就是:
回收 = 将itemView存到List里。
复用 = 在List里取itemView(ListView缓存的是itemView,但是RecyclerView缓存的其实是ViewHolder,而ViewHolder是itemView的一个包装,四舍五入都是缓存的itemView)

image.png

那么啥是四级缓存呢

四级缓存就是RecyclerView根据itemView不同状态,功能,需求等,弄的四类集合(准确的说是5个集合)。

重点!!!

image.png

都是存ViewHolder的ArrayList

image.png

mViewCacheExtension
这个一般给自定义用的,所以不研究

RecycledViewPool

DEFAULT_MAX_SCRAP = 5 也就是ArrayList<ViewHolder>的最大size=5 image.png

也就是说缓存池里ViewHolder最多存5个,在多了就直接废弃。
image.png

也就是说一行数量>5的话,上下滑动,会一直触发Adapter.onCreateViewHolder,因为缓存不够用了。

image.png

源码分析

复用

复用就是从集合里取出ViewHolder

  • 入口:滑动 Move 事件 --> scrollByInternal --> scrollStep --> mLayout.scrollVerticallyBy
  • --> scrollBy --> fill --> layoutChunk --> layoutState.next --> addView(view);
  1. 入口在手指滑动的时候 --- onTouchEvent的Move事件

image.png

  1. scrollByInternal(翻译 内部)

image.png

  1. scrollStep(翻译 步骤) 直接看纵向滚动 image.png

  2. 进入到了LinearLayoutManager.java scrollVerticallyBy

image.png

  1. scrollBy

image.png

  1. fill(翻译 铺满)

image.png

  1. layoutChunk (翻译 块) 先获取View(itemView,也就是缓存里取出来的ViewHolder中的itemView)然后addView到RecyclerView上

image.png

  • layoutState.next --> getViewForPosition --> tryGetViewHolderForPositionByDeadline
  1. layoutState.next 根据mCurrentPosition去取View

image.png

  1. getViewForPosition

image.png

  1. tryGetViewHolderForPositionByDeadline,重点!!! 翻译(尝试获取ViewHolder根据Position 在有效期内)

image.png

tryGetViewHolderForPositionByDeadline -- 重点--四级缓存取的重点

这个方法就是从缓存里取出ViewHolder的方法

他分几种情况去获取ViewHolder

1.getChangedScrapViewForPosition -- mChangeScrap 与动画相关
2.getScrapOrHiddenOrCachedHolderForPosition -- mAttachedScrap 、mCachedViews
3.getScrapOrCachedViewForId -- mAttachedScrap 、mCachedViews (ViewType,itemid)
4.mViewCacheExtension.getViewForPositionAndType -- 自定义缓存
5.getRecycledViewPool().getRecycledView -- 从缓冲池里面获取

  1. getChangedScrapViewForPosition 获取ChangedScrapView通过Position

是从mChangeScrap的List里取的,一般与动画相关 --- 一级缓存

image.png

通过position或者id取,都是for循环遍历List,然后出去holder,在if判断,如果符合条件就返回holder

image.png

  1. 如果没取到,if (holder == null),那就getScrapOrHiddenOrCachedHolderForPosition(获取Scrap/Hidden/Cached的Holder--For--Position)

mAttachedScrap 、mCachedViews从这两列表里取,
mAttachedScrap=一级缓存,mCachedViews=二级缓存

image.png

  1. 如果还是没取到,getScrapOrCachedViewForId,跟第二步一样,只不过第二步用的Position取,这个用的id取
    mAttachedScrap 、mCachedViews (ViewType,itemid)
    mAttachedScrap=一级缓存,mCachedViews=二级缓存

image.png

  1. 还没取到 mViewCacheExtension.getViewForPositionAndType 在自定义缓存里取,一般用不到

mViewCacheExtension三级缓存

image.png

  1. 还没取到去RecycledViewPool里取

RecycledViewPool是第四级缓存,他的构造上面说了

image.png

image.png

  1. 四级缓存都没取到,mAdapter.createViewHolder,那就调用mAdapter的createViewHolder去创建

image.png
image.png

  1. 创建了ViewHolder以后就得去绑定,tryBindViewHolderByDeadline

image.png

最后会调到自己写的Adapter中的onBindViewHolder

image.png

多级缓存的目的:肯定是为了性能

回收

回收就是当ItemView划出屏幕的时候,将ViewHolder存到集合(List)里。

  • LinearLayoutManager.onLayoutChildren --> detachAndScrapAttachedViews --> scrapOrRecycleView
  1. LinearLayoutManager.onLayoutChildren

onLayoutChildren这个方法其实跟onLayout差不多,但是onLayout需要处理很多代码,但是onLayoutChildren其实是简化了onLayout。

image.png

补充(onLayout会调到onLayoutChildren,如果自定义LayoutManager,那重写onLayoutChildren就省去很多onLayout的代码):

RecyclerView.onLayoutChildren里备注了,如果要自定义LayoutManager那就一定要实现这个方法,但是有点坑,谷歌应该定义成抽象的,缺没有

image.png

onLayoutChildren这个方法的起点就是onLayout()

image.png image.png image.png

  1. RecyclerView.detachAndScrapAttachedViews

image.png

  1. scrapOrRecycleView(废弃 or 回收 View)

image.png

  1. itemView的回收的主流程就在scrapOrRecycleView

image.png

scrapOrRecycleView

具体选择哪个缓存,scrap还是RecycleView

if (viewHolder.isInvalid()) 如果viewHolder被移出屏幕那就走
->recycler.recycleViewHolderInternal(viewHolder); (Internal内部)

else -> recycler.scrapView(view);

具体流程:
--> ①.recycler.recycleViewHolderInternal(viewHolder); -- 处理 CacheView 、RecyclerViewPool 的缓存

  • --> 1.ViewHodler改变 不会进来 -- 先判断mCachedViews的大小
    --> mCachedViews.size 大于默认大小 --- recycleCachedViewAt
    -- >addViewHolderToRecycledViewPool --- 缓存池里面的数据都是从mCachedViews里面出来的

  • --> 2.addViewHolderToRecycledViewPool --> getRecycledViewPool().putRecycledView(holder);
    --> scrap.resetInternal(); ViewHolder 清空

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

recycler.recycleViewHolderInternal(viewHolder) -- 处理 CacheView 、RecyclerViewPool 的缓存(scrapOrRecycleView方法里的 if 分支)
  1. 先判断mCachedViews列表的大小是不是大于规定的最大值mViewCacheMax,大于的话就走recycleCachedViewAt()

image.png

  1. 不管mCachedViews大小有没有超最大值,都会将holder,add进mCachedViews列表

image.png

  1. 看如果超过最大mViewCacheMax的recycleCachedViewAt方法

注意入参是0
image.png

1.先 ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
2.将取出的viewHolder,传入addViewHolderToRecycledViewPool(viewHolder, true);--顾名思义:添加ViewHolder到缓存池
3.将mCachedViews.remove(cachedViewIndex)

image.png

总结:

image.png

  1. addViewHolderToRecycledViewPool

image.png

  1. putRecycledView

所以RecycledViewPool里面的ViewHolder是个空的,但是mCachedViews列表里的ViewHolder却是有数据的ViewHolder

image.png

image.png

recycler.scrapView(view) (scrapOrRecycleView方法里的 else 分支)

如果不是划出屏幕的缓存

其实就很简单了,直接往mAttachedScrap或者mChangedScrap列表里add(holder),第一级缓存

image.png

总结

缓存与复用.png

仿探探划卡片

效果:

DFE2346FC33B758466DC7FB09281B934.gif

实现自定义LayoutManager

先实现一个

这就没啥好说的。 7355F4670EB2240AF6F281B2413799B9.jpg

重点在于自定义LayoutManager,之前使用的是系统的LinearLayoutManager

  1. 继承RecyclerView.LayoutManager

自定义LayoutManager,都是需要继承这个的
image.png

  1. 继承完以后发现有一个方法要实现

设置默认的LayoutParams

image.png

查看LinearLayoutManager是怎么做的
直接复制过来就行,自己的也这样实现 image.png

  1. onLayout必须实现,上面有说过,要自定义LayoutManager必须要实现onLayoutChildren,谷歌写的log里说的,但是他没给弄成抽象

image.png

  1. 在onLayoutChildren主要实现的就是布局onLayout,还有参考LinearLayoutManager他还实现了RecyclerView四级缓存的回收、复用都实现了。

回收:看上面源码分析的步骤,回收里调用的是detachAndScrapAttachedViews

image.png

查看LinearLayoutManager源码,复用其实也有调用到,fill()方法

image.png

fill一直往下点(看上面源码分析:)

--> fill --> layoutChunk --> layoutState.next --> addView(view); image.png

layoutState.next。这个就是跳出LinearLayoutManager执行到RecyclerView的方法。
是一个while循环然后遍历,在.next取出各个View,通过
final View view = recycler.getViewForPosition(mCurrentPosition);

image.png

所以我们自己写自定义LayoutManager的onLayoutChildren的时候,回收复用就仿照LinearLayoutManager来
回收=detachAndScrapAttachedViews
复用=for遍历然后通过position取出View,最后在addView,
View view = recycler.getViewForPosition(mCurrentPosition)这个

  1. 回收复用写完

image.png

算上被划走的那张卡片,其实只需要布局4张卡片

image.png

依次减小的三张+最后一张=最底下那张(也就是最下面其实是两张一样大小的)
image.png

先布局最底下的那张卡片,然后依次向上布局,对应list也就是布局0,1,2,3

image.png

  1. onlayout

image.png

  1. 现在布局完了就是item在屏幕中间叠在一起,需要有减小的效果,得把卡片向下平移再缩小一点

image.png

现在的效果就是一个静态的

image.png

自定义ItemTouchHelper

需要能上下左右滑动,需要自定义ItemTouchHelper

ItemTouchHelper一个是滑动,一个是拖拽。现在只需要滑动

  1. extends ItemTouchHelper.SimpleCallback

在拖拽方法啥也不动,滑动方法把划走的DataBean删了,然后在添加回来刷新列表(数据多可以不添回来,我这循环播放所以添回来)

image.png

  1. 设置回弹时间-getAnimationDuration

image.png

  1. 实现onDraw

onChildDraw在这个方法里能取到滑动的(x,y)的值,所以根据勾股定理就能得到滑动的距离。
能得到滑动的百分比,然后通过这个百分比的值,能让下面的卡片按照规律放大顶上来。

如果滑动距离小于一个值,那就让他滑动失败,不然就划走(ItemTouchHelper已经实现了,所以不用自己实现)

image.png