这篇文章我们讲RecycleView的缓存提取和回收,这是RecyclerView的核心部分,也是精髓部分。为什么叫RecycleView也是因为可以进行回收和复用。
关于这块的内容,有几个有代表性的问题
Q1: 都说RecycleView缓存比ListView更强大,强大在何处呢?
Q2: 说的四级缓存都是什么?为什么要设置四级缓存?
Q3: 回收和复用的时机如何?
Q4: 回收和复用的具体逻辑如何?
这篇我们采用自顶向下的方法,先看大体的整体流程,再对细节进行逐个打击。
也可以自己带着自己的问题,欢迎讨论。
缓存数据结构
RecycleView整体的缓存是有4级,逻辑都集中在RecyclerView.Recycle类中。
缓存 | 功能 |
---|---|
AttachedScrap | 缓存detach的ViewHolder |
CachedViews | 缓存可以直接使用,不需要重新bind的ViewHolder |
ViewCacheExtension | 外部可以自己处理的自定义缓存插件 |
RecycledViewPool | 缓存需要重新bind的ViewHolder |
其余还有mScrapList、mChangedScrap、mHiddenViews都是与动画有关的,我们以后分析动画有关的会细讲。
缓存提取都是通过自上而下的顺序,依次从各个部分提取ViewHolder。并通过ViewHolder的状态判断是否可用,不可用就继续向下找。
同样回收也是自上而下,上面存不下了,就存下面。
下面的分析会忽略动画的部分,动画的部分和正常的缓存逻辑混在一起,会影响正常的缓存逻辑的分析。
预备知识
涉及到具体的代码前,有些小细节我们需要提前掌握。要不有些重要的判断无法理解。
ViewHolder状态
我们使用的ViewHolder的状态有很多,我们需要先清楚每个状态的意思,深入看代码的时候才可以理解当时的场景。下表会说明各个状态的意思,并说明代表性的用处。
状态 | 意义 |
---|---|
isBound | 这个 ViewHolder 已经bind了一个position; mPosition、mItemId 和 mItemViewType 都是有效的。onBindViewHolder后会标记。 |
needsUpdate | 这个 ViewHolder 的视图所反映的数据是陈旧的,需要被重新bind。 mPosition 和 mItemId 是有效的。notifyItemChanged后会标记。 |
isInvalid | 此 ViewHolder 的数据无效。mPosition和mItemId无效。 这个ViewHolder 必须进行重新bound。在重新setAdapter/notifyDataSetChanged后,所有的ViewHolder都会被标记。或者在提取缓存后,发现缓存不合法,也会标记。 |
isRemoved | 此ViewHolder表示之前从数据集中删除的项目的数据,notifyItemRemoved后会标记 |
wasReturnedFromScrap | 从mAttachedScrap获取的,会加这个标记 |
isTmpDetached | 当视图从父视图暂时分离时,设置这个标志。 |
detach的概念
上面的状态结合平常的使用都很好理解,唯独isTmpDetached,可能在使用RecyclerView的过程中,没听说过。这里说下detach概念。
我们平时都在使用remove操作,也就是调用ViewGroup的removeView进行子View与父View的彻底分离。但是还有一个detach操作,是一种轻量级的分离,只会把这个View在ViewGroup的children数组中的引用设为null,并设置他的parent为null。当一个视图被分离时,它的父级为 null 并且不能通过调用getChildAt(int)来检索。
调用ViewGroup#detachViewFromParent
会调用下面的方法进行移除。
private void removeFromArray(int index) {
final View[] children = mChildren;
final int count = mChildrenCount;
if (!(mTransitioningViews != null && mTransitioningViews.contains(children[index]))) {
children[index].mParent = null;
}
if (index == count - 1) {
children[--mChildrenCount] = null;
} else if (index >= 0 && index < count) {
children[--mChildrenCount] = null;
}
}
分离视图之后应该调用attachViewToParent(View, int, ViewGroup.LayoutParams)进行重新连接。 分离应该只是暂时的需要注意的是,重新连接或和分离应在与分离相同的绘图周期内进行。 如果在一个绘画周期内,如果只是暂时的分离,后面极大的概率会再加入,那么使用detach更加的高效。
知道了上面的基础概念,我们分析源码也会更加的容易。先分析下缓存使用的时机,也就是什么时候会进行缓存的操作。
缓存回收时机
在进行布局时回收
会全局的进行统一回收,也就是对所有子View进行detach,如果布局完成后,还在显示区域内显示,那么就会重新attach。其他不显示的就会被remove。
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
。。。
detachAndScrapAttachedViews(recycler);
。。。
}
detachAndScrapAttachedViews内部会调用scrapOrRecycleView进行回收。这一步的回收,主要是回收进一级缓存。
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
。。。
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {
removeViewAt(index);
recycler.recycleViewHolderInternal(viewHolder);
} else {
detachViewAt(index);
recycler.scrapView(view);
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
}
}
上面的判断逻辑下面会细分析。
滑动过程中会进行回收
上一篇讲滑动的时候,也分析过。滑动的过程最终会调用recycleViewHolderInternal()进行回收,当一个View超出了RecyclerView的边界时,就会进行回收。内部主要在第二级和第四级缓存进行了回收后的填充。这里没有对第三级mViewCacheExtension进行回收,可见Recycle不会默认填充我们自定义的ViewCacheExtension。
public void recycleViewHolderInternal(@NonNull View child, @NonNull Recycler recycler) {
。。。
if(。。。){
//第二级 CachedViews
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
if (!cached) {
//第四级 RecycledViewPool
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
}
。。。
}
我们发现进行布局操作回收时,内部回收会进行判断,是执行recycleViewHolderInternal()还是scrapView()。
detach操作会进行scrapView(),而remove操作会进行recycleViewHolderInternal()。
在recycleViewHolderInternal()中,我们没有找到第一级缓存的填充位置,其实就在scrapView()中进行的。
可见滑动滑动过程中的填充是不会涉及到第一级缓存的。并且第一级填充是只回收被detach的View。
这里可以看出回收就两种操作
- 执行scap回收detach的View。对应一级缓存。
- 执行recycle回收remove的View。对应二、四级缓存。
缓存提取时机
什么情况下会进行缓存册提取,肯定是用到的时候会进行提取。第一次填充、滑动填充、刷新数据填充都要进行缓存的提取。填充的逻辑前面的布局章节已经讲到了,会调用LayoutManager的fill里面,内部一系列调用RecyclerView.Recycler的getViewForPosition方法,最终调用tryGetViewHolderForPositionByDeadline方法进行提取。先看下内部大致的逻辑。
// dryRun:如果不应从废料/缓存中删除 ViewHolder,则为 True
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
。。。
if (holder == null) {
// 提取一二级缓存
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
。。。
}
if (holder == null) {
if (mAdapter.hasStableIds()) {
//hasStableIds为true下 提取一二级缓存
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
。。。
}
if (holder == null && mViewCacheExtension != null)
// 提取三级缓存
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
。。。
}
if (holder == null) {
// 提取四级缓存
holder = getRecycledViewPool().getRecycledView(type);
。。。
}
if (holder == null) {
。。。
holder = mAdapter.createViewHolder(RecyclerView.this, type);
。。。
}
}
。。。
if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
// 进行bindView
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
。。。
return holder;
}
上面的代码省略了很多逻辑,但是保留了大体的缓存提取的框架。很清晰的可以看到缓存的提取过程
- 通过getScrapOrHiddenOrCachedHolderForPosition获取mAttachedScrap和mCachedViews内的缓存,
- mViewCacheExtension.getViewForPositionAndType获取CacheExtension的缓存
- 通过item的type用getRecycledViewPool().getRecycledView方法获取缓存
- 如果以上步骤都不能拿到,那么就我们数字的createViewHolder方法,创建一个。 之后判断如果需要绑定数据,就调用tryBindViewHolderByDeadline进行bindView。
- 通过上面的步骤,拿到holder以后,会尝试进行bindView,什么情况下不需要bind?即holder.isBound() && !holder.needsUpdate() && !holder.isInvalid()。可结合上面的ViewHolder的状态进行分析。也就是已经bind过了,并且不需要update(调用了notifyUpdate),不是无效的(调用了notifyDataSetChanged等)。
解决了什么时候使用缓存的问题。下面我们来依次详细的分析下4级缓存,并分别从填充和提取两个重要的方面进行讨论。
一级缓存:AttachedScrap
回收
上面分析整体流程时,已经知道了,一级缓存的填充主要在scrapView()中进行。
void scrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
。。。
holder.setScrapContainer(this, false);
mAttachedScrap.add(holder);
。。。
}
我们看填充的逻辑比较简单。填充的时机上面也说了,就是在执行LayoutManager的onLayoutChildren()时,会调用detachAndScrapAttachedViews调用scrapOrRecycleView回收所有的View,这时就会调用scrapView进行一级缓存的填充。但也不是所有的View都进入一级缓存。要满足一个条件。我们看scrapOrRecycleView的代码。
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
。。。
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {
removeViewAt(index);
recycler.recycleViewHolderInternal(viewHolder);
} else {
detachViewAt(index);
recycler.scrapView(view);
}
}
在ViewHolder不合法 且 数据没有被移除 且 不是hasStableIds模式下,会调用remove,进行第二级和第四级的填充。具体的case有兴趣可以研究下。
提取
一级缓存和二级缓存的提取是在一个函数中的,所以下面的提取逻辑和使用逻辑对二级缓存也是一致的。 一级缓存提取的逻辑主要是在getScrapOrHiddenOrCachedHolderForPosition和getScrapOrCachedViewForId中。getScrapOrHiddenOrCachedHolderForPosition是常规的提取操作,而getScrapOrCachedViewForId则对应hasStableIds为true的情况。
ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
final int scrapCount = mAttachedScrap.size();
for (int i = 0; i < scrapCount; i++) {
final ViewHolder holder = mAttachedScrap.get(i);
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
&& !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
。。。
// 从二级缓存CachedViews中获取
}
getScrapOrCachedViewForId同上面的逻辑类似,区别就是通过getItemId和itemViewType的标记进行提取。
ViewHolder getScrapOrCachedViewForId(long id, int type, boolean dryRun) {
final int count = mAttachedScrap.size();
for (int i = count - 1; i >= 0; i--) {
final ViewHolder holder = mAttachedScrap.get(i);
if (holder.getItemId() == id && !holder.wasReturnedFromScrap()) {
if (type == holder.getItemViewType()) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
}
。。。
// 从二级缓存CachedViews中获取
}
使用
提取到合适的ViewHolder之后,我们需要看下怎么使用的。
检查合法性
主要通过validateViewHolderForOffsetPosition检查合法性,
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if (holder != null) {
if (!validateViewHolderForOffsetPosition(holder)) {
//检测非法
if (!dryRun) {
// dryRun为false需要删除废料,这里设置FLAG_INVALID标记,并下放进行recycle回收
holder.addFlags(ViewHolder.FLAG_INVALID);
if (holder.isScrap()) {
removeDetachedView(holder.itemView, false);
holder.unScrap();
} else if (holder.wasReturnedFromScrap()) {
holder.clearReturnedFromScrapFlag();
}
recycleViewHolderInternal(holder);
}
holder = null;
} else {
fromScrapOrHiddenOrCache = true;
}
}
boolean validateViewHolderForOffsetPosition(ViewHolder holder) {
。。。
if (holder.mPosition < 0 || holder.mPosition >= mAdapter.getItemCount()) {
throw new IndexOutOfBoundsException("Inconsistency detected. Invalid view holder "
+ "adapter position" + holder + exceptionLabel());
}
if (!mState.isPreLayout()) {
final int type = mAdapter.getItemViewType(holder.mPosition);
if (type != holder.getItemViewType()) {
return false;
}
}
if (mAdapter.hasStableIds()) {
return holder.getItemId() == mAdapter.getItemId(holder.mPosition);
}
return true;
}
如果检测holder不合法,那么会把拿到的holder置空,表示还没有拿到合适的holder,如果dryRun为false,表示需要删除废料,内部会下放holder到下游的缓存中。validateViewHolderForOffsetPosition检查合法的逻辑也比较简单。
- 首先是这个holder的position需要合法,也就是不能超过getItemCount()返回的总量。这里我们看到了一个熟悉的异常:Inconsistency detected. Invalid view holder adapter position。如果在填充中,我们使itemCount变小,那么这里可能就会产生崩溃。变大则无事。说明我们需要同步内外部数据。不一致可能会崩溃。
- itemViewType要一致
- 检查了hasStableIds()的情况,itemId要一致。
不合法则从下游缓存中继续找合适的缓存。
如果检测holder是合法的,那么就回进行下一步的正式的布局了。
布局
拿到holder后返回到LayoutManager的fill方法内部,并调用addView,进而调用addViewInt进行填充。
private void addViewInt(View child, int index, boolean disappearing) {
final ViewHolder holder = getChildViewHolderInt(child);
。。。
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (holder.wasReturnedFromScrap() || holder.isScrap()) {
if (holder.isScrap()) {
holder.unScrap();
} else {
holder.clearReturnedFromScrapFlag();
}
mChildHelper.attachViewToParent(child, index, child.getLayoutParams(), false);
}
。。。
}
void unscrapView(ViewHolder holder) {
mAttachedScrap.remove(holder);
holder.mScrapContainer = null;
holder.mInChangeScrap = false;
holder.clearReturnedFromScrapFlag();
}
可以看到拿到的holder#wasReturnedFromScrap()会返回true,isScrap()内部判断setScrapContainer是否为nul,在填充时,每个holder都会执行setScrapContainer(),所以也会返回true。那么就先执行unScrap(),再执行attachViewToParent()。
unScrap()内部从一级缓存中删除了这个ViewHolder,并清空了填充时的各个标记位。attachViewToParent()是detach的逆向操作。完成这两个操作后,这个View又重新detach到了RecycleView上。
通过以上的分析,我们知道了一级缓存的填充、回收、使用。整体的流程都已经清晰了,分析其他各级缓存时,也会按照这个流程进行谈论。但是我们还不知道为什么这么设计,设计特点如何。我们还要总结下一级缓存的用途和特点。
一级缓存特点
一级缓存只缓存detach的View,这种操作是临时的。滑动过程中的回收和提取不涉及这一级缓存。并通过postion的匹配进行提取。主要应用在布局开始中的清空操作,再在重新布局中立即取出进行填充。
我们知道View会进行两次Layout,RecycleView为了优化这个过程,直接使用第一级缓存进行直接回收。意义在于快速重用屏幕上可见的列表项ItemView,在这种情况下,不需要重新createView和bindView。
二级缓存:CachedViews
回收
CachedViews回收填充的逻辑主要在recycleViewHolderInternal()中
DEFAULT_CACHE_SIZE = 2;
int mViewCacheMax = DEFAULT_CACHE_SIZE;
void recycleViewHolderInternal(ViewHolder holder) {
。。。
if (mViewCacheMax > 0
&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_REMOVED
| ViewHolder.FLAG_UPDATE
| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
int cachedViewSize = mCachedViews.size();
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
recycleCachedViewAt(0);
cachedViewSize--;
}
int targetCacheIndex = cachedViewSize;
if (ALLOW_THREAD_GAP_WORK
&& cachedViewSize > 0
&& !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
// 添加视图时,跳过最近预取的视图
int cacheIndex = cachedViewSize - 1;
while (cacheIndex >= 0) {
int cachedPos = mCachedViews.get(cacheIndex).mPosition;
if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
break;
}
cacheIndex--;
}
targetCacheIndex = cacheIndex + 1;
}
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
。。。
// cached为false下,填充第四级缓存RecycledViewPool
}
mViewCacheMax是二级缓存的最大容量,通过updateViewCacheSize()进行计算得到,外部setViewCacheSize()设置容量二级缓存容量时会调用。
mViewCacheMax由两部分相加组成,缓存的容量mRequestedCacheMax和预取的容量extraCache。
mRequestedCacheMax可以通过setViewCacheSize()设置,表示缓存的最大容量。预取容量extraCache由预加载控件GapWorker自适应控制。所以我们调节二级缓存的大小,使用setViewCacheSize()即可。
void updateViewCacheSize() {
int extraCache = mLayout != null ? mLayout.mPrefetchMaxCountObserved : 0;
mViewCacheMax = mRequestedCacheMax + extraCache;
for (int i = mCachedViews.size() - 1;
i >= 0 && mCachedViews.size() > mViewCacheMax; i--) {
recycleCachedViewAt(i);
}
}
我们看到满足一个条件才可以添加进二级缓存,也就是最大容量大于0并且要添加的ViewHolder不能有代码中4个状态中的一个。满足条件后进行填充。
如果容量满了,还需要recycle掉第一个缓存,也就是下放缓存到之后的三四级缓存。默认的添加位置是CachedViews的最后一个,但是要跳过预取的postion。这里讲预取时,会详细讲解。
最后就在mCachedViews中填充了这个缓存。
提取
提取的逻辑是和一级缓存AttachedScrap提取在一起的。逻辑很类似。getScrapOrHiddenOrCachedHolderForPosition和getScrapOrCachedViewForId中都有相似的逻辑。这里不在赘述了。
ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
。。。
// 提取一级缓存,没有继续提取二级缓存
final int cacheSize = mCachedViews.size();
for (int i = 0; i < cacheSize; i++) {
final ViewHolder holder = mCachedViews.get(i);
if (!holder.isInvalid() && holder.getLayoutPosition() == position
&& !holder.isAttachedToTransitionOverlay()) {
return holder;
}
}
。。。
使用
使用的逻辑是和一级缓存AttachedScrap类似,也是线检查合法性。再进行布局填充。填充逻辑有差异,我们单独看下填充的逻辑。同样是调用addViewInt进行填充。
private void addViewInt(View child, int index, boolean disappearing) {
final ViewHolder holder = getChildViewHolderInt(child);
。。。
if (holder.wasReturnedFromScrap() || holder.isScrap()) {
//对应一级缓存的填充
。。。
mChildHelper.attachViewToParent(child, index, child.getLayoutParams(), false);
} else if (child.getParent() == mRecyclerView) {
// addView已经现有的View,这里会进行交换
if (currentIndex != index) {
mRecyclerView.mLayout.moveView(currentIndex, index);
}
} else {
//对应除一级缓存,其余缓存的使用
mChildHelper.addView(child, index, false);
lp.mInsetsDirty = true;
if (mSmoothScroller != null && mSmoothScroller.isRunning()) {
mSmoothScroller.onChildAttachedToWindow(child);
}
}
。。。
}
这里对应第三种条件判断情况,直接调用了mChildHelper.addView()。而不像一级缓存进行attachViewToParent操作。 mChildHelper.addView()内部直接调用了RecycleView的addView操作。逻辑比较简单
二级缓存特点
这一级缓存会在屏幕滑动时进行填充和回收,所以意义在于缓存离开屏幕的ItemView,目的是让即将进入屏幕的ItemView重用。
三级缓存:ViewCacheExtension
ViewCacheExtension是一个接口,只有一个抽象方法:提取View
public abstract static class ViewCacheExtension {、
@Nullable
public abstract View getViewForPositionAndType(@NonNull Recycler recycler, int position,
int type);
}
回收
上面也讲到了,RecycleView默认不会对ViewCacheExtension进行填充。开发人员有责任决定他们是要在这个自定义缓存中保留他们的视图还是让默认的回收策略来处理它。
提取
提取的逻辑比较简单,直接调用抽象方法getViewForPositionAndType,怎么取就是我们自己的实现。
四级缓存:RecycledViewPool
先介绍下RecycledViewPool,它内部主要使用SparseArray这个数据结构。这个结构不熟悉的可以类比HashMap,他是安卓提供的数据结构,相比HashMap进行了优化。有兴趣可以自己了解下。存储SparseArray的Key就是每个ItemView的itemType,value是一个ArrayList,内部存放了ViewHolder。每个value能存放的最大容量默认是5,我们可以通过setMaxRecycledViews(int viewType, int max)自定义大小。
回收
RecycledViewPool填充的逻辑和二级缓存一起,在recycleViewHolderInternal()中。
void recycleViewHolderInternal(ViewHolder holder) {
。。。
// 填充二级缓存,cached为true表示填充成功
if (!cached) {
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
}
}
void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {
clearNestedRecyclerViewIfNotNested(holder);
View itemView = holder.itemView;
。。。
if (dispatchRecycled) {
// 回调onViewRecycled
dispatchViewRecycled(holder);
}
holder.mOwnerRecyclerView = null;
getRecycledViewPool().putRecycledView(holder);
}
public void putRecycledView(ViewHolder scrap) {
final int viewType = scrap.getItemViewType();
final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
return;
}
scrap.resetInternal();
scrapHeap.add(scrap);
}
填充的逻辑比较简单,结合RecycledViewPool内部的数据结构,可以看到先取出相应ItemType的List,再进行填充。这里要注意,如果相应的List等于最大长度时。就不会填充了,这点和二级缓存CachedViews不同,二级缓存会清空第一个,继续填充。
这里需要注意的就是加入缓存前,会调用resetInternal(),也就是重制内部的所有的状态。这样提取出来会就需要重新的bind了。
提取
提取的逻辑在tryGetViewHolderForPositionByDeadline()内部。
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
// 从一级、二级缓存中提取
if (holder == null) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
final int type = mAdapter.getItemViewType(offsetPosition);
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
// 还没有,直接createView
}
public ViewHolder getRecycledView(int viewType) {
final ScrapData scrapData = mScrap.get(viewType);
if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
for (int i = scrapHeap.size() - 1; i >= 0; i--) {
if (!scrapHeap.get(i).isAttachedToTransitionOverlay()) {
return scrapHeap.remove(i);
}
}
}
return null;
}
内部通过getItemViewType获取itemViewType,然后到内部去进行匹配合适的ViewHolder。拿到 holder后,会调用resetInternal到初始状态,所以这种情况下拿到的holder一定是需要重新bind的。 我们发现这里不是直接使用的position,而是重新计算了一个findPositionOffset。使用这个进行匹配。findPositionOffset内部会通过我们notify局部刷新相关的数据,重新正确非位置。比如说列表上面的元素删除了或增加了,这时候正确的position也会不一样。
为什么这里需要考虑局部刷新的数据。而从第一第二级缓存中拿不需要考虑局部刷新,直接使用了position呢?
因为第一第二级缓存中都是通过position来直接获取的,外部需要给定position对应的View。不需要关心最新的位置,给定的位置肯定是准确的。而从RecycledViewPool获取,我们是通过itemViewType,如果外部数据发生变化,那么我们通过初试position拿到的itemViewType可能是错误的,所以需要用最新的position。这个逻辑和getLayoutPosition()和getAdapterPosition()的区别是一致的,getLayoutPosition()获取在布局中的位置,getAdapterPosition()获取在最新的数据中的位置。
特点
从第四级缓存中取出的ViewHolder是一定要重新bind的。
经过对上面四级缓存,从上游到下游的缓存提取后。就会尝试进行bindView
缓存的清空,迁移
缓存还有一些清空和迁移情况,迁移问题比较特殊,是一种下沉,上层把缓存删除给下层继续缓存。当通过setAdapter更改适配器时,会发生。更改了适配器缓存肯定失效了,所以这我们会删除所有的缓存。RecycleView内部会触发一系列操作进行删除。
- 通过removeAndRecycleScrapInt()和recycleAndClearCachedViews()方法,一级/二级缓存迁移到四级缓存。
- 清空一级/二级缓存
- 通过getRecycledViewPool().onAdapterChanged() 触发清空四级缓存
这就达到了清空所有缓存的目的,有兴趣的同学可以。 有时我们想更改适配器,但是还想复用原来的缓存,我们可以使用swapAdapter,它只会进行1,2两部。不会进行第三部。
还有一些操作也会对缓存进行清空
一级缓存的清空
有几种情况会对第一级缓存进行清空操作,操作就是调用mAttachedScrap的clear方法。 在dispatchLayoutStep3会进行清空。我们发现一级缓存的填充是在onLayoutChildren中进行,也就是 dispatchLayoutStep2时。到了dispatchLayoutStep3就会清空。所以它的生命周期就是这两个方法之间。
下一篇讲局部刷新的原理和代码细节。