本文,笔者通过动态调试,来理解RV的缓存机制,并记录其重要成员的变化,来完全解读RV的实现过程。感兴趣的读者可将本文作为试错参考,去主动阅读源码。
RecyclerView的核心是缓存复用,因此必须搞清楚Recycker的几个缓存池。笔者根据其注释以及实际调试,得到如下结论:
mAttachedScrap是仍附着在RecyclerView的ViewHolder,可见。
mChangedScrap也仍附着在RecyclerView中,可见,不过由于调用了诸如notifyItemChange*类方法,需将更改的holder放入此池中。
笔者提醒读者注意,以上两种scrap只有在调用itemChange类方法时才存在值,滑动时为空。当其中的view被复用后,即移除。因此,以上两种scrap只短暂存在过view,其缓存意义是调用notifyItemChange*方法时,避免频繁创建viewHolder。
mCacheViews中的ViewHolder已经从RecyclerView移除,默认大小2,主要在滑动复用的使用。mViewCacheExtension自定义扩展缓存。
mRecyclerPool(mScrap)可以理解为最后一层缓存。
笔者在这里简单写一个demo,证明上述结论的正确性;
以上每个位置都是一个TextView,均设置了点击事件调用notifyItemChanged(position),传入当前position作为参数。长按时直接调用notifydataSetChanged()。
操作一如下,点击position31,得到如下信息。
mCacheViews中存在28、29。
mChangedScrap存在31,即点击的position。
mAttachedScrap是除mChangedScrap中所有可见的position,但其中也包括不可见的61,笔者这次注意到最近一次滑动是向下滑动,猜测mCacheViews中的缓存是滑动存入。
操作二如下,长按任一position,得到如下信息
可以看到,mAttachedScrap、mCacheViews、mChangedScrap均为空,只有mScrap中存在5个值,且mMaxScrap等于5,笔者在此猜测当所有item都失效时(notifyDataSetChanged),会将item缓存到mScrap中,mScrap是一个key-value的sparseArray,key为item-type,由于此处笔者的demo默认type为默认值,所以只能看到如上结果。
入口是notifyItemDataSetChanged,全局刷新。
我们跟进,
直接调用到RecyclerViewDataObsrver#onChanged方法,第一行assert断言不在layout过程且不在滚动状态,否则抛出异常。mStructureChanged标记true。
跟进processDataSetCompletelyChanged。
从中,根据注释以及实际逻辑,标记一些flag,并且将mCacheView中的ViewHolder标记为过时|脏,如下。
笔者在这里理解,因为发生了全局更新,之前缓存的值已经失去了缓存的意义,因此标记为无效缓存。
hasPendingUpdages是命令集合,此处笔者假设只单独调用了此方法,因此进入if分支,进一步调用requestLayout方法。
从注释知,这里的mInterceptRequestLayoutDepth是layout次数,在每个dispatchLayoutStepX方法中都会自增,这里是想阻塞重复requestLayout。笔者假设第一次调用,则会请求到View#requestLayout。而View#requestLayout经过一系列View绘制流程,从VIewRootImpl深度遍历到RecyclerView,调用其onLayout函数,我们跟进。
核心在dispatchLayout,我们跟进。
当不存在mAdapter或mLayout时,直接返回。
dispatchLayoutSetp1、Step2是什么意思呢?笔者在此处暂时忽略step1,稍后补充。我们进入step2看看,
step2,看上去是实际layout过程,而mLayout是LayoutManager,我们以线性布局为例,进入查看。
detachAndScrapAttachedViews是detach掉当前所有的item,当发生了item改变,或者全局刷新时,均会调用到此处。我们跟进,
由于全局刷新时,如上文所述,标记全面view为invalid,且未从RV中移除,因此进入第一个if分支,随后第一个removeViewAt是真正从RV中移除子view,随后调用recycler.recyckeVuewHolderInternal回收viewHolder,我们跟进,
已经标记为INVALID,cached为false,调用addViewHolderToRecycleViewPool,我们跟进,
从中可以发现,对于viewType,目前默认缓存最大值是5,这样就将一些Holder缓存到了mScrap中。
记下来,已经回收完ViewHolder,回到onLayoutChild,进入fill查看。
由于全局刷新,且无滚动逻辑,第一个红框函数不会执行,直接看layoutChunk函数,
我们来看下如何拿到itemView,跟进next方法,
注意,mCurrentPosition在每次layoutChild时,都会++mItemDirection,因此将此position理解为view在layout中排放的绝对位置。最终哈,我们调用到tryGetViewHolderForPositionByDeadline方法,
该方法是复用缓存逻辑的一个关键。
1,如果满足条件,尝试从mChangeScrap中获取,通常是change item项。
2,尝试用position从mAttachScrap或mCacheView中获取,
3,尝试用type从mAttachScrap或mCacheView中获取获取,
5,尝试从mViewCacheExtension中获取,这是用户自定义的缓存。
5,最后,尝试从RVPool中获取,
如果上述5种方法仍无法获取到holder,则创建
bind过程接上,
脏的holder,需要更新的holder才需要重新绑定,从mAttachedScrap中获取的holder不需要,跟进,
跟到这,单个itemView已经获取并且bind成功。
接下来,我们回到layoutChunk,看看mAttachedScrap和mChangedScrap怎么移除view,
通过以上过程我们知道,在重新attach到RV前,先从mChangedScrap和mAttachedScrap中移除了holder。
笔者全局刷新逻辑暂分析到此,后面就进入draw流程。但这仅仅只是第一次添加元素,接下来我们看下滑动复用逻辑,这才是RV的关键。
这里笔者假设正常滑动(忽略嵌套滑动逻辑),onInterceptTouchEvent返回true,从onToucheEvent出发,
当滑动成功是,要求parent view不能拦截事件,我们跟进
进入scrollStep,
笔者假设此次是垂直滑动,mLayout仍是线性布局器,进入scrollVerticallyBy
直接进入fill,
由于存在滑动,此时mScrollingOffset不为无效值,则调用到recycleByLayoutStae
假设回收start view,跟进,
跟进,满足条件的view会调用到recycleChildren,如此次滑动的positio31,
removeViewAt是移除RV中的View,recycleView是回收view到缓存中,我们跟进,
如果holder已经被废弃、或还未从parent中移除、或只是临时detach、或应该忽略都抛出异常,
当mCacheViews中存放超过mViewCacheMax,即默认2,移除index 0 位置的view,随后添加新的cache view,但要注意如下条件。
holder非invalid、非removed、非脏、非位置未知时,才添加到cache中,否则放入RecycledViewPool中。即即将隐藏的边界item,
ok,当所有边界item被缓存到mCacheView中时,我们回到fill处继续分析,
layoutChunk处添加viewHolder,不赘述。
当所有的viewHolder都准备好后,再设置所有viewChild的偏移,即实现了我们所看见的RV视图。
如上图,笔者假设某次滑动传入偏移43,我们跟进,
遍历所有childView,getChild返回的就是itemView,最后直接调用到View#offsetTopAndBottom方法,就完成的所有itemView的偏移。
笔者注意到,RV实现了嵌套滑动child接口,如下
那么,RV是支持嵌套滑动的,其作用为滑动Child,与实现NestedScrollingParent接口的View交互。只需开启如下接口即可,
具体的嵌套滑动逻辑,读者可参考接口定义。