RecyclerView缓存复用解析,源码解读
Posted wodongx123
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了RecyclerView缓存复用解析,源码解读相关的知识,希望对你有一定的参考价值。
文章目录
RecyclerView有四级缓存,其中一级缓存是用户自定义的缓存,四级缓存本质都是内存,是按照功能区分的。
名称 | 数据结构 | 容量 | 作用 |
mAttachedScrap | ArrayList | 看界面上能显示几个 | 存放界面上显示的View |
mCachedViews | ArrayList | 2 | 用于移除屏幕外的视图的回收与复用 |
mViewCacheExtension | 自定义 | 自定义缓存,用户定制 | |
mRecyclerPool | SparseArray | 5 | 缓存池,用户移出屏幕View的回收和复用,会重置ViewHolder的数据 |
1. 缓存回收复用的原理
在RecyclerView中,四级缓存其实都是存放在内存中的数据,所以他的分级都是按照逻辑上的功能来分级的。
1.1 为什么要有四级缓存,每一级缓存的作用
一级缓存
mAttachedScrap 其实就是用户当前页面上直接可以看到的item,他们都被存放在同一个列表中以便于Recycler管理
二级缓存
mCachedViews 保存的是,刚刚移出屏幕的View,一共保存2个,他们存在的意义在于,用户如果在往下滑动一点点以后,又忽然反悔了往上滑动的时候,保证原先的那个视图可以快速的展示给用户显示,因为该View的位置以及内容都是曾经被绘制过的,所以不需要重新更新数据。
三级缓存
用户自定义缓存,我们一般不用
四级缓存
回收池,这个是一个很关键的内容,在我们实际的项目中,一旦设计到某个需要频繁的创建销毁的对象时,都会采用池的设计方式,将一定数量的对象保存起来,如果需要用到的时候,直接从池子里面取,而不需要重新创建一个新的。
目的是在于通过数据保存的方式防止频繁创建对象造成的内存抖动,从而频繁引发GC造成卡顿。
1.2 四级缓存是如何工作的
我们先把场景模拟好:我们的手机屏幕,正好可以放置五个item。每个场景都是往下滑动一个item(不会画动图)。
-
加载第一屏:
当刚进入页面的时候,由于我们的四级缓存都是空的,所以第一屏显示的5个item都是当场创建出来的。
第一屏显示5个的话,就意味着,屏幕内最多可以存放6个item(上下各显示一部分),当我们手指稍微往下滑动一点,第6个item也就被创建,并且放入一级缓存中。
-
滑动加载更多,CachedView:当我们往下滑动的时候要去加载第7个item的时候,这个时候由于只有一级缓存是有视图的,所以我们也会新创建一个item用于显示,这个时候,第1个item被隐藏起来了,用户看不到了第1个item会被存放到mCachedView中,这样如果用户往下滑动一点点之后如果忽然反悔了,我们可以快速展示前面出现过的item。
-
滑动加载更多,回收池:此时,屏幕上和mCachedView都已经存满了,用户又开始往下滑动,这次就会触发完整的缓存回收复用机制,
回收:RecyclerView会将,mCachedView的第一个(其实就是最早被加入到mCachedView的)视图,转移到回收池中。
复用:需要新加载的View,RecyclerView会尝试重回收池里面去取出View,重新写入数据后加入到RecyclerView中。
从图可以看出,一个一屏能显示6个item的页面,正常情况下一共会创建9个item,如果你滑的很快,或者场景比较复杂的,回收池里面的item会更多,但是不会超过回收池的上限5个。
2. 源码时序图和解读
如何去看RecyclerView滑动的源码,就要看启用这个缓存功能的起点,起点自然是滑动事件,本质也是点击事件,所以起点是RecyclerView.onTouchEvent
时序图如下:
把里面的关键方法都再看一遍,顺便整理一下里面的一些细节。
我会只截取有意义的代码部分,并且调整不同方法的顺序,让我们看的时候可以直接从上往下按顺序看。
如果某个方法中,我有进行省略代码的行为,我会注释表明,反之就是没有省略代码。
如果因为省略代码而看到了意义不明的参数,请忽略他,他并不会影响源码的阅读。
2.1 缓存回收
虽然时序图是从OnTouchEvent开始画的,但是真正的逻辑处理是从LinearLayoutManager开始,所以直接从recycleByLayoutState方法开始看
LinearLayoutManager
public class LinearLayoutManager extends RecyclerView.LayoutManager implements
ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider
/** 根据LayoutState的状态来回收视图*/
private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState)
//省略了无关代码
//判断当前是往上滑动还是往下滑动
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START)
recycleViewsFromEnd(recycler, scrollingOffset, noRecycleSpace);
else
recycleViewsFromStart(recycler, scrollingOffset, noRecycleSpace);
/** 回收滚动到布局末尾后出界的视图。检查布局位置和可见位置,以确保视图不可见。*/
private void recycleViewsFromStart(RecyclerView.Recycler recycler, int scrollingOffset,
int noRecycleSpace)
//省略了无关代码
final int limit = scrollingOffset - noRecycleSpace;
final int childCount = getChildCount();
// 从最上面的视图开始遍历,找到第一个不需要回收的视图
for (int i = 0; i < childCount; i++)
View child = getChildAt(i);
if (mOrientationHelper.getDecoratedEnd(child) > limit
|| mOrientationHelper.getTransformedEndWithDecoration(child) > limit)
recycleChildren(recycler, 0, i);
return;
/**
* 回收指示以内的所有视图
*/
private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex)
if (startIndex == endIndex)
return;
if (endIndex > startIndex)
for (int i = endIndex - 1; i >= startIndex; i--)
// 转到RecyclerView处理
removeAndRecycleViewAt(i, recycler);
else
for (int i = startIndex; i > endIndex; i--)
removeAndRecycleViewAt(i, recycler);
RecyclerView这个类高度解耦,可以设置横纵,或者网格类型的数据实体,所以我们在看滑动的方法调用时,要从具体的LayoutManager开始看。
LinearLayoutManager在回收机制中做的事代码看着非常复杂,但是实际上做的事很简单。以往下滑动为例,manager无非就是,把所有完全不可见的视图集合起来,去调用RecyclerView的removeAndRecyclerView方法来进行实际的回收。
RecyclerView
public class RecyclerView extends ViewGroup implements ScrollingView,
NestedScrollingChild2, NestedScrollingChild3
/**
* 移除一个子View并且交由Recycler来回收它
*/
public void removeAndRecycleViewAt(int index, @NonNull Recycler recycler)
final View view = getChildAt(index);
removeViewAt(index);
recycler.recycleView(view);
/** RecyclerView内部的回收类,用于做View的回收机制 */
public final class Recycler
/** 回收一个View */
public void recycleView(@NonNull View view)
//省略了无关代码
ViewHolder holder = getChildViewHolderInt(view);
recycleViewHolderInternal(holder);
void recycleViewHolderInternal(ViewHolder holder)
//省略了无关代码
final boolean forceRecycle = mAdapter != null
&& transientStatePreventsRecycling
&& mAdapter.onFailedToRecycleView(holder);
if (forceRecycle || holder.isRecyclable())
if (mViewCacheMax > 0
&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_REMOVED
| ViewHolder.FLAG_UPDATE
| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN))
// 获取CachedViews的数量,如果超过上限,就先把CachedViews的第一个元素给放到回收池了
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;
if (!cached)
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
/** 将CachedView中指定下标的View回收 */
void recycleCachedViewAt(int cachedViewIndex)
ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
addViewHolderToRecycledViewPool(viewHolder, true);
mCachedViews.remove(cachedViewIndex);
/** 将视图加入到回收池里面 */
void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled)
//省略了无关代码
holder.mOwnerRecyclerView = null;
getRecycledViewPool().putRecycledView(holder);
具体的回收就涉及到他的多级缓存了
在回收目标View的时候,他会先判断,能否把将这个View,放入到CachedViews中,如果可以的话,就会将CachedView中的第一个View,转移到回收池里面,在将目标View放入到CachedView
如果不能的情况,就将目标View直接放到回收池里面
2.2 缓存复用
为了方便源码解读,我们统一的场景为竖直排列且向下滑动
LinearLayoutManager
public class LinearLayoutManager extends RecyclerView.LayoutManager implements
ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result)
// 省略了无关代码
// 通过LayoutState去获取一个View
View view = layoutState.next(recycler);
if (view == null)
result.mFinished = true;
return;
// 将获取到的View添加到视图上面
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
if (layoutState.mScrapList == null)
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START))
addView(view);
else
addView(view, 0);
else
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START))
addDisappearingView(view);
else
addDisappearingView(view, 0);
/** 在LayoutManager填充空白时保持临时状态的Helper类 */
static class LayoutState
/**
* Current position on the adapter to get the next item.
*/
int mCurrentPosition;
View next(RecyclerView.Recycler recycler)
// 省略了无关代码
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
当我们往下滑动的时候,那么下面不是要显示一个新的View吗,layoutChunk这个方法就是先通过四级缓存机制去拿到这个View,然后再直接加到RecyclerView上面,就做了这么简单的事,每次要新显示一个View,就会调用一次这个方法。
RecyclerView
public class RecyclerView extends ViewGroup implements ScrollingView,
NestedScrollingChild2, NestedScrollingChild3
public final class Recycler
/** 一级缓存,存放的是不需要重新绑定就可以重用的视图 */
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
/** 一级缓存,存放的是发生改变的Scrap视图,如果重用,需要重新经过adapter的绑定 */
ArrayList<ViewHolder> mChangedScrap = null;
/** 二级缓存mCachedViews */
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
/** 三级缓存mViewCacheExtension,一般用不上 */
private ViewCacheExtension mViewCacheExtension;
/** 四级缓存mRecyclerPool */
RecycledViewPool mRecyclerPool;
@NonNull
public View getViewForPosition(int position)
return getViewForPosition(position, false);
View getViewForPosition(int position, boolean dryRun)
return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
/** 根据下标来获取ViewHolder */
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs)
//省略了无关代码
boolean fromScrapOrHiddenOrCache = false;
ViewHolder holder = null;
// 1 从非常规缓存mChangedScrap获取
if (mState.isPreLayout())
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
// 2 从mAttachedScrap或者mCachedViews获取
if (holder == null)
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if (holder == null)
// 2 从mAttachedScrap或者mCachedViews获取,和上面不同的是
// 这次是根据视图的id来获取,不是根据视图的位置
// 根据id来获取的功能,要设置了才能生效,一般是不生效的
if (mAdapter.hasStableIds())
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
if (holder != null)
// update position
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
// 3 从mViewCacheExtension获取
if (holder == null && mViewCacheExtension != null)
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
if (view != null)
holder = getChildViewHolder(view);
// 4 从缓存池获取
if (holder == null)
holder = getRecycledViewPool().getRecycledView(type);
// 前面4步都找不到的情况下,创建一个新的
if (holder == null)
holder = mAdapter.createViewHolder(RecyclerView.this, type);
return holder;
具体如何获取View的方法,不用过多文字说明,基本就是根据缓存的优先级一级一级的去找,找不到的情况下,就创建一个新的
关于mChangedScrap
mChangedScrap 有人说是一级缓存,有人说不是,但是tryGetViewHolderForPositionByDeadline这个方法,最先判断的就是他
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs)
....
if (mState.isPreLayout())
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
....
mPreLayout这个字段,默认是false,也就是说一般情况下不会走到这个逻辑里面,那什么时候会走到该逻辑呢
简单来说会涉及到RecyclerView的预布局和动画的原理,这里就先略过。
public class RecyclerView extends ViewGroup implements ScrollingView,
NestedScrollingChild2, NestedScrollingChild3
final State mState = new State();
protected void onMeasure(int widthSpec, int heightSpec)
//省略了无关代码
if (mAdapterUpdateDuringMeasure)
if (mState.mRunPredictiveAnimations)
mState.mInPreLayout = true;
else
// consume remaining updates to provide a consistent state with the layout pass.
mAdapterHelper.consumeUpdatesInOnePass();
mState.mInPreLayout = false;
public static class State
/**
* True if the associated @link RecyclerView is in the pre-layout step where it is having
* its @link LayoutManager layout items where they will be at the beginning of a set of
* predictive item animations.
*/
boolean mInPreLayout = false;
2.3 回收池结构
public static class RecycledViewPool
/** 每种类型的最大存放数量*/
private static final int DEFAULT_MAX_SCRAP = 5;
static class ScrapData
final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
long mCreateRunningAverageNs = 0;
long mBindRunningAverageNs = 0;
SparseArray<ScrapData> mScrap = new SparseArray<>();
@Nullable
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以上是关于RecyclerView缓存复用解析,源码解读的主要内容,如果未能解决你的问题,请参考以下文章