文章目录
- 1. 缓存回收复用的原理
- 1.1 为什么要有四级缓存,每一级缓存的作用
- 一级缓存
- 二级缓存
- 三级缓存
- 四级缓存
- 1.2 四级缓存是如何工作的
- 2. 源码时序图和解读
- 2.1 缓存回收
- LinearLayoutManager
- RecyclerView
- 2.2 缓存复用
- LinearLayoutManager
- RecyclerView
- 关于mChangedScrap
- 2.3 回收池结构
- 附:时序图代码
- 参考材料
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.get(i).isAttachedToTransitionOverlay()) {
return scrapHeap.remove(i);
}
}
}
return null;
}
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);
}
}
这个池是一层SparseArray包着一层ArrayList
外层的SparseArray的key是viewType,也就是说,我们创建的不同类型的视图在回收池里面是分开存的。
内层的ArrayList就是具体存ViewHolder的,最大容量为5,当存的时候如果超过最大容量了,就会放弃这次保存,直接丢弃。
附:时序图代码
@startuml
participant RecyclerView
participant LinearLayoutManager
participant RecyclerView.Recycler as recycler
participant LayoutState
RecyclerView -> RecyclerView : onTouchEvent
activate RecyclerView
RecyclerView -> RecyclerView : scrollByInternal
activate RecyclerView
RecyclerView -> RecyclerView : scrollStep
activate RecyclerView
RecyclerView -> RecyclerView : scrollStep
RecyclerView -> LinearLayoutManager : scrollVerticallyBy
activate LinearLayoutManager
LinearLayoutManager -> LinearLayoutManager : scrollBy
LinearLayoutManager -> LinearLayoutManager : fill(关键方法)
alt 这部分是缓存的回收机制
activate LinearLayoutManager
LinearLayoutManager -> LinearLayoutManager : recycleByLayoutState
activate LinearLayoutManager
LinearLayoutManager -> LinearLayoutManager : recycleViewsFromStart(也有FromEnd方法)
LinearLayoutManager -> LinearLayoutManager : recycleChildren
LinearLayoutManager -> RecyclerView : removeAndRecycleViewAt
activate RecyclerView
RecyclerView -> recycler : recycleView
activate recycler
recycler -> recycler : recycleViewHolderInternal
activate recycler
deactivate recycler
deactivate recycler
deactivate RecyclerView
end
alt 这部分是缓存的复用机制
deactivate LinearLayoutManager
LinearLayoutManager -> LinearLayoutManager : layoutChunk
activate LinearLayoutManager
LinearLayoutManager -> LayoutState : next
activate LayoutState
LayoutState -> recycler : getViewForPosition
activate recycler
recycler -> recycler : tryGetViewHolderForPositionByDeadline
deactivate LayoutState
end
deactivate RecyclerView
deactivate RecyclerView
deactivate RecyclerView
@enduml
参考材料
码牛学院
2021.12.13-RecycleView回收模型-四级缓存-David