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

谈到 RecyclerView,相信不少同学,张口都能说出它的几级缓存机制:

例如:

  • 一级缓存:mAttachedScrap 和 mChangedScrap
  • 二级缓存:mCachedViews
  • 三级缓存:ViewCacheExtension
  • 四级缓存:RecycledViewPool

然后说怎么用,就是先从 1 级找,然后 2 级...然后4 级,找不到 create ViewHolder。

那么,有没有思考过,其实上面几级缓存都属于“内存缓存",那么这么分级肯定有一定区别。

问题来了:

  1. 每一级缓存具体作用是什么?
  2. 分别在什么场景下会用到哪些缓存呢?

 

先来温习一下RecyclerView的滚动和回收机制:

RecyclerView之所以能滚动,就是因为它在监听到手指滑动之后,不断地更新Item的位置,也就是反复layout子View了,这部分工作由LayoutManager负责。

LayoutManager在layout子View之前,会先把RecyclerView的每个子View所对应的ViewHolder都放到mAttachedScrap中,然后根据当前滑动距离,筛选出哪些Item需要layout。获取子View对象,会通过getViewForPosition方法来获取。这个方法就是题目中说的那样:先从mAttachedScrap中找,再......

Item布局完成之后,会对刚刚没有再次布局的Item进行缓存(回收),这个缓存分两种:

  1. 取出(即将重用)时无须重新绑定数据(即不用执行onBindViewHolder方法)。这种缓存只适用于特定position的Item(名花有主);
  2. 取出后会回调onBindViewHolder方法,好让对应Item的内容能正确显示。这种缓存适用所有同类型的Item(云英未嫁);

第一种缓存,由Recycler.mCachedViews来保管,第二种放在RecycledViewPool中。

如果回收的Item它的状态(包括:INVALID、REMOVED、UPDATE、POSITION_UNKNOWN)没有变更,就会放到mCachedViews中,否则扔RecycledViewPool里。

emmmm,除了滚动过程中,会对Item进行回收和重新布局,还有一种就是,当Adapter数据有更新时:

  1. Inserted:如果刚好插入在屏幕可见范围内,会从RecycledViewPool中找一个相同类型的ViewHolder(找不到就create)来重新绑定数据并layout;

  2. Removed:会把对应ViewHolder扔到mAttachedScrap中并播放动画,动画播放完毕后移到RecycledViewPool里;

  3. Changed:这种情况并不是大家所认为的:直接将这个ViewHolder传到Adapter的onBindViewHolder中重新绑定数据。而是先把旧的ViewHolder扔mChangedScrap中,然后像Inserted那样从RecycledViewPool中找一个相同类型的ViewHolder来重新绑定数据。旧ViewHolder对象用来播放动画,动画播完,同样会移到RecycledViewPool里;

注意:如果是使用notifyDataSetChanged方法来通知更新的话,那么所有Item都会直接扔RecycledViewPool中,然后逐个重新绑定数据的。

当然了,上面说的这几种情况,不包括瞎写自定义的LayoutManager,因为在自定义的LayoutManger中,怎么去管理缓存,完全出于个人喜好。

好啦,温习完了之后,来看回题目中的问题:

这几个存放缓存的集合,各自的作用以及使用场景?

  • mAttachedScrap:LayoutManager每次layout子View之前,那些已经添加到RecyclerView中的Item以及被删除的Item的临时存放地。使用场景就是RecyclerView滚动时、还有在可见范围内删除Item后用notifyItemRemoved方法通知更新时;

  • mChangedScrap:作用:存放可见范围内有更新的Item。使用场景:可见范围内的Item有更新,并且使用notifyItemChanged方法通知更新时;就是临时缓存局部更新,用于播放动画,动画播放完viewholder还是会给 recyclerpool

  • mCachedViews:作用:存放滚动过程中没有被重新使用且状态无变化的那些旧Item。场景:滚动,prefetch

  • RecycledViewPool:作用:缓存Item的最终站,用于保存那些RemovedChanged、以及mCachedViews满了之后更旧的Item。场景:Item被移除、Item有更新、滚动过程;

写到这里发现漏讲了一个prefetch,好吧,这个prefetch机制就是RecyclerView在滚动和惯性滚动的时候,借助Handler来事先从RecycledViewPool中取出即将要显示的Item,随即扔到mCachedViews中,这样的话,当layout到这个Item时,就能直接拿来用而不用绑定数据了。

 

 

为什么我没有说ViewCacheExtension?

因为我发现,这个东西我们开发者根本不能通过常规手段来使用!!!

为什么这么说呢?

星期五那晚特意网上搜了一下关于自定义ViewCacheExtension的文章,但是一篇相关的都没有,甚至官方的库也搜不到,Github上也搜过了,没有!

本来我的想法是这样的:

看到大家的回答都没有针对ViewCacheExtension做解释,就想着根据自己的理解,补充一个自定义ViewCacheExtension的示例:

但是这个抽象类它没有put,只有get,那就只能自己去获取缓存了,在哪里获取呢?

想了一下,有两个地方比较合适(实际上是一个地方):

  1. 重写Adapter的onViewRecycled方法;
  2. 直接给RecyclerView set一个RecyclerListener;

不过我们要做的这个缓存,并不打算缓存普通的Item,因为普通Item,现有的LayoutManager就已经做得很好了,我们应该用这个去缓存一些Bind比较耗时的,或者一些内容不会变(可以共享)Item。

emmm,理想很丰满,当我动手做时:

    java.lang.IllegalArgumentException: Scrapped or attached views may not be recycled. isScrap:false isAttached:true androidx.recyclerview.widget.RecyclerView{993f0ac VFED..... .F.....D 0,0-1080,1584 #7f080105 app:id/recyclerView}
        at androidx.recyclerview.widget.RecyclerView$Recycler.recycleViewHolderInternal(RecyclerView.java:6433)
        at androidx.recyclerview.widget.RecyclerView$Recycler.recycleView(RecyclerView.java:6369)
        at androidx.recyclerview.widget.GapWorker.prefetchPositionWithDeadline(GapWorker.java:295)
        at androidx.recyclerview.widget.GapWorker.flushTaskWithDeadline(GapWorker.java:345)
        at androidx.recyclerview.widget.GapWorker.flushTasksWithDeadline(GapWorker.java:361)
        at androidx.recyclerview.widget.GapWorker.prefetch(GapWorker.java:368)
        at androidx.recyclerview.widget.GapWorker.run(GapWorker.java:399)
        at android.os.Handler.handleCallback(Handler.java:790)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:164)
        at android.app.ActivityThread.main(ActivityThread.java:6494)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)

哈哈哈哈哈,就是因为用了回收之后的Item,它的Scrap状态没有去掉,当再次被回收时,就报这个错了。

那RecyclerView内部是怎么处理的呢:

public final void bindViewHolder(@NonNull VH holder, int position) {
    ......
    holder.setFlags(ViewHolder.FLAG_BOUND,
            ViewHolder.FLAG_BOUND | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID
                    | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN);

    onBindViewHolder(holder, position, holder.getUnmodifiedPayloads());
    ......
}

在回调onBindViewHolder之前,会重置这些状态标记。

这时候可能你会想到:在自定义的ViewCacheExtension取出来之前,手动把这些状态重置不就行了?

没门!

void setFlags(int flags, int mask) {
   mFlags = (mFlags & ~mask) | (flags & mask);
}

ViewHolder的setFlags方法访问权限是default。

现在你可能会想:既然这样,那就干脆不用它回收之后的Item,直接用Adapter的createViewHolder来事先创建不行吗?

还真是不行,因为在取出View之后,会对它进行验证:

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());
    }
}

如果这个View的LayoutParams的mViewHolder实例为空的话,还是会报错的。

那。。那我再手动赋值呢?

不好意思,LayoutParams的mViewHolder访问权限也是default。

也就是用常规方式(目前)是无法使用ViewCacheExtension了,想过用反射,但是,缓存这东西的目的就是要提高效率,为了能使用ViewCacheExtension而去做降低效率的事情,那就得不偿失了,除非你bind view的时间比反射所需时间多得多。

以后跟同学们谈论,说到ViewCacheExtension的时候,你就可以大声说:“别想了,这东西根本用不了的!Google弄出来这个就没想让我们用!”

RecyclerView 的观察者模式:

RecyclerView 持有 mObserver 观察者对象,本质为 RecyclerViewDataObserver

Adaper 持有 mObservable 被观察对象,本质为 AdapterDataObservable。

调用 RecyclerView.setAdapter(adapter)时将 mObservable.reqisterObserver(observer)

调用 adapter.notifydataChange 或者是 notifyitemchanged,实际上是通过observable 遍历

observer 调用 onchanged 然后 requestlayout 重新 layout 子view

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值