RecyclerView 的缓存机制,可谓是面试中的常客了。不仅如此,在使用过程中,如果了解这个缓存机制,那么可以更好地利用其特性做开发。
那么,我们将以场景化的方式,讲解 RecyclerView 的缓存机制。常见的两个场景是:
本文将讲解 滑动 RecyclerView 下 的缓存机制
背景知识:负责回收和复用 ViewHolder 的类是 Recycler,负责缓存的主要就是这个类的几个成员变量。我们贴点源码看看(下面源码的注释(和我写的注释),很重要,要记得认真看哦)
/**
* A Recycler is responsible for managing scrapped or detached item views for reuse.
* A "scrapped" view is a view that is still attached to its parent RecyclerView but that has been marked for removal or reuse.
*
* Typical use of a Recycler by a RecyclerView.LayoutManager will be to obtain views
* for an adapter's data set representing the data at a given position or item ID.
* If the view to be reused is considered "dirty" the adapter will be asked to rebind it.
* If not, the view can be quickly reused by the LayoutManager with no further work.
* Clean views that have not requested layout may be repositioned by a LayoutManager without remeasurement.
*/
public final class Recycler {
final ArrayList mAttachedScrap = new ArrayList<>();// 存放可见范围内的 ViewHolder (但是在 onLayoutChildren 的时候,会将所有 View 都会缓存到这), 从这里复用的 ViewHolder 如果 position 或者 id 对应的上,则不需要重新绑定数据。
ArrayList mChangedScrap = null;// 存放可见范围内并且数据发生了变化的 ViewHolder,从这里复用的 ViewHolder 需要重新绑定数据。
final ArrayList mCachedViews = new ArrayList(); // 存放 remove 掉的 ViewHolder,从这里复用的 ViewHolder 如果 position 或者 id 对应的上,则不需要重新绑定数据。
private int mRequestedCacheMax = DEFAULT_CACHE_SIZE; // 默认值是 2
int mViewCacheMax = DEFAULT_CACHE_SIZE; // 默认值是 2
RecycledViewPool mRecyclerPool; // 存放 remove 掉,并且重置了数据的 ViewHolder,从这里复用的 ViewHolder 需要重新绑定数据。 // 默认值大小是 5
private ViewCacheExtension mViewCacheExtension; // 自定义的缓存
}
至于到底有几级缓存,我觉得这个问题不大重要。有人说三层,有人说四层。有人说三层,因为觉得自定义那层,不是 RecyclerView 实现的,所以不算;也有人认为 Scrap 并不是真正的缓存,所以不算。
从源码看来,我更同意后者,Scrap 不算一层缓存。因为在源码中,mCachedViews 被称为 first-level。至于为什么 Scrap 不算一层,我的理解是:因为这层的只是 detach 了,并没有 remove,所以这层也没有缓存大小的概念,只要符合规则就会加入进去。
// Search the first-level cache
final int cacheSize = mCachedViews.size();
类型 | 变量名 | 存储说明 | 备注 |
Scrap | mAttachedScrap | 存放可见范围内的 ViewHolder | 从这里复用的 ViewHolder 如果 position 或者 id 对应的上,则不需要重新绑定数据。在 onLayoutChildren 的时候,会将所有 View 都会缓存到这 |
mChangedScrap | 存放可见范围内并且数据发生了变化的 ViewHolder | 从这里复用的 ViewHolder 需要重新绑定数据。 | |
Cache | mCachedViews | 存放 remove 掉的 ViewHolder | 从这里复用的 ViewHolder 如果 position 或者 id 对应的上,则不需要重新绑定数据。 |
ViewCacheExtension | mViewCacheExtension | 自定义缓存 | |
RecycledViewPool | mRecyclerPool | 存放 remove 掉,并且重置了数据的 ViewHolder | 从这里复用的 ViewHolder 需要重新绑定数据。 |
通过 Android Studio 的 Profiles 工具,我们可以看到调用流程
入口是 ouTouchEvent
通过表格的方式,简要说明上图的流程都在做什么?
方法名 | 隶属的类 | 作用描述 |
onTouchEvent() | RecyclerView | 处理点击事件,在 MOVE 事件中在一定条件下,拦截事件后,做事件处理 |
scrollByInternal() | RecyclerView | 主要是调用 scrollStep() |
scrollStep() | RecyclerView | 通过 dx 和 dy 的值判断是调用scrollHorizontallyBy()还是 scrollVerticallyBy() |
scrollHorizontallyBy()/scrollVerticallyBy() | LayoutManager | 主要是调用 scrollBy() |
scrollBy() | LayoutManager | 通过调用 fill() 添加滑进来的View 和回收滑出去的 View |
offsetChildrenVertical()/offsetChildrenHorizontal() | RecyclerView | 做偏移操作 |
通过上述表格,我们知道了。最重要的东西那就是 scrollBy 中调用了 fill 的方法了。那我们看看 fill 在做什么吧?滑出去的 View 最后去哪里了呢?滑进来的 View 是怎么来的?(带着这个问题,我们一起来读源码!一定要带着),源码只留下了核心部分
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
// max offset we should set is mFastScroll + available
final int start = layoutState.mAvailable;
//首选该语句块的判断,判断当前状态是否为滚动状态,如果是的话,则触发 recycleByLayoutState 方法
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
// TODO ugly bug fix. should not happen
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
// 分析1----回收
recycleByLayoutState(recycler, layoutState);
}
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
//分析2----复用
layoutChunk(recycler, state, layoutState, layoutChunkResult);
}
}
// 分析1----回收
// 通过一步步追踪,我们发现最后调用的是 removeAndRecycleViewAt()
public void removeAndRecycleViewAt(int index, @NonNull Recycler recycler) {
final View view = getChildAt(index);
//分析1-1
removeViewAt(index);
//分析1-2
recycler.recycleView(view);
}
// 分析1-1
// 从 RecyclerView 移除一个 View
public void removeViewAt(int index) {
final View child = getChildAt(index);
if (child != null) {
mChildHelper.removeViewAt(index);
}
}
//分析1-2
// recycler.recycleView(view) 最终调用的是 recycleViewHolderInternal(holder) 进行回收 VH (ViewHolder)
void recycleViewHolderInternal(ViewHolder holder) {
if (forceRecycle || holder.isRecyclable()) {
//判断是否满足放进 mCachedViews
if (mViewCacheMax > 0 && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID| ViewHolder.FLAG_REMOVED| ViewHolder.FLAG_UPDATE| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)){
// 判断 mCachedViews 是否已满
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
// 如果满了就将下标为0(即最早加入的)移除,同时将其加入到 RecyclerPool 中
recycleCachedViewAt(0);
cachedViewSize--;
}
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
//如果没有满足上面的条件,则直接存进 RecyclerPool 中
if (!cached) {
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
}
}
//分析2
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
//分析2-1
View view = layoutState.next(recycler);
if (layoutState.mScrapList == null) {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
//添加到 RecyclerView 上
addView(view);
} else {
addView(view, 0);
}
}
}
//分析2-1
//layoutState.next(recycler) 最后调用的是 tryGetViewHolderForPositionByDeadline() 这个方法正是 复用 核心的方法
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
// 0) If there is a changed scrap, try to find from there
// 例如:我们调用 notifyItemChanged 方法时
if (mState.isPreLayout()) {
// 如果是 changed 的 ViewHolder 那么就先从 mChangedScrap 中找
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
// 1) Find by position from scrap/hidden list/cache
if (holder == null) {
//如果在上面没有找到(holder == null),那就尝试从通过 pos 在 mAttachedScrap/ mHiddenViews / mCachedViews 中获取
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
}
if (holder == null) {
// 2) Find from scrap/cache via stable ids, if exists
if (mAdapter.hasStableIds()) {
//如果在上面没有找到(holder == null),那就尝试从通过 id 在 mAttachedScrap/ mCachedViews 中获取
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
}
if (holder == null && mViewCacheExtension != null) {
//这里是通过自定义缓存中获取,忽略
}
//如果在上面都没有找到(holder == null),那就尝试在 RecycledViewPool 中获取
if (holder == null) { // fallback to pool
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
//这里拿的是,要清空数据的
holder.resetInternal();
}
}
//如果在 Scrap / Hidden / Cache / RecycledViewPool 都没有找到,那就只能创建一个了。
if (holder == null) {
holder = mAdapter.createViewHolder(RecyclerView.this, type);
}
}
return holder;
}
做一个总结,在分析源码前,我们提出了三个问题,那看看答案是什么吧
Q:那我们看看 fill 在做什么吧?
A:其实就是分析1(回收 ViewHolder ) + 分析 2 ( 复用 ViewHolder )
Q:滑出去的 View 最后去哪里了呢?
A:先尝试回收到 mCachedViews 中,未成功,则回收到 RecycledViewPool 中。
Q:滑进来的 View 是怎么来的?
A:如果是 isPreLayout 则先从 mChangedScrap 中尝试获取。
未获取到,再从 mAttachedScrap / mHiddenViews / mCachedViews (通过 position ) 中尝试获取
未获取到,再从 mAttachedScrap / mCachedViews (通过 id)中尝试获取
未获取到,再从 自定义缓存中尝试获取
未获取到,再从 RecycledViewPool 中尝试获取
未获取到,创建一个新的 ViewHolder