RecyclerView完全解读

一,概述

本文,笔者通过动态调试,来理解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)可以理解为最后一层缓存。

RecyclerView完全解读_第1张图片

笔者在这里简单写一个demo,证明上述结论的正确性;

RecyclerView完全解读_第2张图片

以上每个位置都是一个TextView,均设置了点击事件调用notifyItemChanged(position),传入当前position作为参数。长按时直接调用notifydataSetChanged()。

操作一如下,点击position31,得到如下信息。

RecyclerView完全解读_第3张图片

mCacheViews中存在28、29。

mChangedScrap存在31,即点击的position。

mAttachedScrap是除mChangedScrap中所有可见的position,但其中也包括不可见的61,笔者这次注意到最近一次滑动是向下滑动,猜测mCacheViews中的缓存是滑动存入。

操作二如下,长按任一position,得到如下信息

RecyclerView完全解读_第4张图片

可以看到,mAttachedScrap、mCacheViews、mChangedScrap均为空,只有mScrap中存在5个值,且mMaxScrap等于5,笔者在此猜测当所有item都失效时(notifyDataSetChanged),会将item缓存到mScrap中,mScrap是一个key-value的sparseArray,key为item-type,由于此处笔者的demo默认type为默认值,所以只能看到如上结果。

三,全局刷新

入口是notifyItemDataSetChanged,全局刷新。

RecyclerView完全解读_第5张图片

我们跟进,

RecyclerView完全解读_第6张图片

直接调用到RecyclerViewDataObsrver#onChanged方法,第一行assert断言不在layout过程且不在滚动状态,否则抛出异常。mStructureChanged标记true。

跟进processDataSetCompletelyChanged。

RecyclerView完全解读_第7张图片

从中,根据注释以及实际逻辑,标记一些flag,并且将mCacheView中的ViewHolder标记为过时|脏,如下。

RecyclerView完全解读_第8张图片

RecyclerView完全解读_第9张图片

笔者在这里理解,因为发生了全局更新,之前缓存的值已经失去了缓存的意义,因此标记为无效缓存。

RecyclerView完全解读_第10张图片

hasPendingUpdages是命令集合,此处笔者假设只单独调用了此方法,因此进入if分支,进一步调用requestLayout方法。

RecyclerView完全解读_第11张图片

从注释知,这里的mInterceptRequestLayoutDepth是layout次数,在每个dispatchLayoutStepX方法中都会自增,这里是想阻塞重复requestLayout。笔者假设第一次调用,则会请求到View#requestLayout。而View#requestLayout经过一系列View绘制流程,从VIewRootImpl深度遍历到RecyclerView,调用其onLayout函数,我们跟进。

RecyclerView完全解读_第12张图片

核心在dispatchLayout,我们跟进。

当不存在mAdapter或mLayout时,直接返回。

dispatchLayoutSetp1、Step2是什么意思呢?笔者在此处暂时忽略step1,稍后补充。我们进入step2看看,

step2,看上去是实际layout过程,而mLayout是LayoutManager,我们以线性布局为例,进入查看。

RecyclerView完全解读_第13张图片

detachAndScrapAttachedViews是detach掉当前所有的item,当发生了item改变,或者全局刷新时,均会调用到此处。我们跟进,

RecyclerView完全解读_第14张图片

RecyclerView完全解读_第15张图片

由于全局刷新时,如上文所述,标记全面view为invalid,且未从RV中移除,因此进入第一个if分支,随后第一个removeViewAt是真正从RV中移除子view,随后调用recycler.recyckeVuewHolderInternal回收viewHolder,我们跟进,

RecyclerView完全解读_第16张图片

RecyclerView完全解读_第17张图片

已经标记为INVALID,cached为false,调用addViewHolderToRecycleViewPool,我们跟进,

RecyclerView完全解读_第18张图片

RecyclerView完全解读_第19张图片

从中可以发现,对于viewType,目前默认缓存最大值是5,这样就将一些Holder缓存到了mScrap中。

记下来,已经回收完ViewHolder,回到onLayoutChild,进入fill查看。

由于全局刷新,且无滚动逻辑,第一个红框函数不会执行,直接看layoutChunk函数,

RecyclerView完全解读_第20张图片

我们来看下如何拿到itemView,跟进next方法,

RecyclerView完全解读_第21张图片注意,mCurrentPosition在每次layoutChild时,都会++mItemDirection,因此将此position理解为view在layout中排放的绝对位置。最终哈,我们调用到tryGetViewHolderForPositionByDeadline方法,

该方法是复用缓存逻辑的一个关键。

1,如果满足条件,尝试从mChangeScrap中获取,通常是change item项。

2,尝试用position从mAttachScrap或mCacheView中获取,

3,尝试用type从mAttachScrap或mCacheView中获取获取,

5,尝试从mViewCacheExtension中获取,这是用户自定义的缓存。

5,最后,尝试从RVPool中获取,

RecyclerView完全解读_第22张图片

如果上述5种方法仍无法获取到holder,则创建

RecyclerView完全解读_第23张图片这样,对于单个child,itemView就获取完毕。

bind过程接上,

RecyclerView完全解读_第24张图片

脏的holder,需要更新的holder才需要重新绑定,从mAttachedScrap中获取的holder不需要,跟进,

跟到这,单个itemView已经获取并且bind成功。

接下来,我们回到layoutChunk,看看mAttachedScrap和mChangedScrap怎么移除view,

RecyclerView完全解读_第25张图片

通过以上过程我们知道,在重新attach到RV前,先从mChangedScrap和mAttachedScrap中移除了holder。

笔者全局刷新逻辑暂分析到此,后面就进入draw流程。但这仅仅只是第一次添加元素,接下来我们看下滑动复用逻辑,这才是RV的关键

四,滑动复用

这里笔者假设正常滑动(忽略嵌套滑动逻辑),onInterceptTouchEvent返回true,从onToucheEvent出发,

RecyclerView完全解读_第26张图片

当滑动成功是,要求parent view不能拦截事件,我们跟进

RecyclerView完全解读_第27张图片

进入scrollStep,

RecyclerView完全解读_第28张图片

笔者假设此次是垂直滑动,mLayout仍是线性布局器,进入scrollVerticallyBy

RecyclerView完全解读_第29张图片

直接进入fill,

RecyclerView完全解读_第30张图片

由于存在滑动,此时mScrollingOffset不为无效值,则调用到recycleByLayoutStae

RecyclerView完全解读_第31张图片

假设回收start view,跟进,

跟进,满足条件的view会调用到recycleChildren,如此次滑动的positio31,

RecyclerView完全解读_第32张图片

RecyclerView完全解读_第33张图片

removeViewAt是移除RV中的View,recycleView是回收view到缓存中,我们跟进,

如果holder已经被废弃、或还未从parent中移除、或只是临时detach、或应该忽略都抛出异常,

当mCacheViews中存放超过mViewCacheMax,即默认2,移除index 0 位置的view,随后添加新的cache view,但要注意如下条件。

RecyclerView完全解读_第34张图片

holder非invalid、非removed、非脏、非位置未知时,才添加到cache中,否则放入RecycledViewPool中。即即将隐藏的边界item,

RecyclerView完全解读_第35张图片

ok,当所有边界item被缓存到mCacheView中时,我们回到fill处继续分析,

layoutChunk处添加viewHolder,不赘述。

当所有的viewHolder都准备好后,再设置所有viewChild的偏移,即实现了我们所看见的RV视图。

RecyclerView完全解读_第36张图片

如上图,笔者假设某次滑动传入偏移43,我们跟进,

RecyclerView完全解读_第37张图片

遍历所有childView,getChild返回的就是itemView,最后直接调用到View#offsetTopAndBottom方法,就完成的所有itemView的偏移。

五,嵌套滑动

笔者注意到,RV实现了嵌套滑动child接口,如下

RecyclerView完全解读_第38张图片

那么,RV是支持嵌套滑动的,其作用为滑动Child,与实现NestedScrollingParent接口的View交互。只需开启如下接口即可,

RecyclerView完全解读_第39张图片

具体的嵌套滑动逻辑,读者可参考接口定义。

你可能感兴趣的:(java,开发语言)