先来温习一下RecyclerView的滚动和回收机制:
RecyclerView之所以能滚动,就是因为它在监听到手指滑动之后,不断地更新Item的位置,也就是反复layout子View了,这部分工作由LayoutManager负责。
LayoutManager在layout子View之前,会先把RecyclerView的每个子View所对应的ViewHolder都放到mAttachedScrap中,然后根据当前滑动距离,筛选出哪些Item需要layout。获取子View对象,会通过getViewForPosition方法来获取。这个方法就是题目中说的那样:先从mAttachedScrap中找,再…
Item布局完成之后,会对刚刚没有再次布局的Item进行缓存(回收),这个缓存分两种:
取出(即将重用)时无须重新绑定数据(即不用执行onBindViewHolder方法)。这种缓存只适用于特定position的Item(名花有主);
取出后会回调onBindViewHolder方法,好让对应Item的内容能正确显示。这种缓存适用所有同类型的Item(云英未嫁);
第一种缓存,由Recycler.mCachedViews来保管,第二种放在RecycledViewPool中。
如果回收的Item它的状态(包括:INVALID、REMOVED、UPDATE、POSITION_UNKNOWN)没有变更,就会放到mCachedViews中,否则扔RecycledViewPool里。
emmmm,除了滚动过程中,会对Item进行回收和重新布局,还有一种就是,当Adapter数据有更新时:
Inserted:如果刚好插入在屏幕可见范围内,会从RecycledViewPool中找一个相同类型的ViewHolder(找不到就create)来重新绑定数据并layout;
Removed:会把对应ViewHolder扔到mAttachedScrap中并播放动画,动画播放完毕后移到RecycledViewPool里;
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方法通知更新时;
mCachedViews:作用:存放滚动过程中没有被重新使用且状态无变化的那些旧Item。场景:滚动,prefetch;
RecycledViewPool:作用:缓存Item的最终站,用于保存那些Removed、Changed、以及mCachedViews满了之后更旧的Item。场景:Item被移除、Item有更新、滚动过程;
写到这里发现漏讲了一个prefetch,好吧,这个prefetch机制就是RecyclerView在滚动和惯性滚动的时候,借助Handler来事先从RecycledViewPool中取出即将要显示的Item,随即扔到mCachedViews中,这样的话,当layout到这个Item时,就能直接拿来用而不用绑定数据了。
为什么我没有说ViewCacheExtension?
因为我发现,这个东西我们开发者根本不能通过常规手段来使用!!!
为什么这么说呢?
星期五那晚特意网上搜了一下关于自定义ViewCacheExtension的文章,但是一篇相关的都没有,甚至官方的库也搜不到,Github上也搜过了,没有!
本来我的想法是这样的:
看到大家的回答都没有针对ViewCacheExtension做解释,就想着根据自己的理解,补充一个自定义ViewCacheExtension的示例:
但是这个抽象类它没有put,只有get,那就只能自己去获取缓存了,在哪里获取呢?
想了一下,有两个地方比较合适(实际上是一个地方):
重写Adapter的onViewRecycled方法;
直接给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的时间比反射所需时间多得多。