Android中RecyclerView的缓存是面试中的场景问题,为了更好的理解四级缓存的作用,今天带大家一起来从源码的角度来看这块内容。
- 屏内缓存 (mAttachedScrap,mChangedScrap)
- 离屏缓存 (CachedViews)
- 自定义缓存 (mViewCacheExtension)
- 缓存池 (RecycledViewPool)
上述的四级缓存都集中在RecyclerView内部的Recycler类中
public final class Recycler {
// 屏内缓存
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
ArrayList<ViewHolder> mChangedScrap = null;
// 离屏缓存
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
private final List<ViewHolder>
mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);
private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
int mViewCacheMax = DEFAULT_CACHE_SIZE;
// 缓存池
RecycledViewPool mRecyclerPool;
// 自定义缓存
private ViewCacheExtension mViewCacheExtension;
...
屏内缓存 (mAttachedScrap,mChangedScrap)
跟随源码中的mAttachedScrap 和 mChangedScrap ,主要看两者在哪里add的。可以找到如下方法:
/**
* Mark an attached view as scrap. (将一个attach View 标记为废弃,这里的attach就是显示在Rv上的)
*
* <p>"Scrap" views are still attached to their parent RecyclerView but are eligible
* for rebinding and reuse. Requests for a view for a given position may return a
* reused or rebound scrap view instance.</p>
* //"Scrap" 视图仍附加在它们的父级 RecyclerView 上但符合重新绑定和重用的条件。
* // 对于给定位置的视图请求可能会返回一个重用的或重新绑定的 scrap 视图实例。
* @param view View to scrap
*/
void scrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view); // 获取ViewHolder
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
|| !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {
throw new IllegalArgumentException("Called scrap view with an invalid view."
+ " Invalid views cannot be reused from scrap, they should rebound from"
+ " recycler pool." + exceptionLabel());
} // 验证ViewHolder的可用性。 未变化,或可以复用 updated 的 → mAttachedScrap
holder.setScrapContainer(this, false);
mAttachedScrap.add(holder);
} else { // 数据已变化且不能复用 → mChangedScrap
if (mChangedScrap == null) {
mChangedScrap = new ArrayList<ViewHolder>();
}
holder.setScrapContainer(this, true);
mChangedScrap.add(holder);
}
}
明白了两者是在scrapView方法中添加缓存,且mAttachedScrap代表的是未改变的ViewHolder,mChangedScrap代表的是改变的ViewHolder。接下来一起看哪里调用了scrapView方法。
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
if (viewHolder.shouldIgnore()) {
if (sVerboseLoggingEnabled) {
Log.d(TAG, "ignoring view " + viewHolder);
}
return;
}
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {
// ViewHolder 已无效 + 未被移除 + 没有 stable ids
// 直接回收进 Pool
removeViewAt(index);
recycler.recycleViewHolderInternal(viewHolder);
} else {
detachViewAt(index);
recycler.scrapView(view);// 调用scrapView进行回收
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
}
}
那scrapOrRecycleView是怎么被调用的呢?继续往上找可以发现是 detachAndScrapView系列方法调用的。detachAndScrapView方法的调用链如下:
notifyDataSetChanged / notifyItemInserted / requestLayout / 首次显示
└→ RecyclerView.requestLayout()
└→ onLayout()
└→ dispatchLayout()
└→ dispatchLayoutStep1() ← pre-layout
└→ mLayout.onLayoutChildren(mRecycler, mState)
└→ detachAndScrapAttachedViews(recycler) ← 在这里
└→ scrapOrRecycleView() × N(每个 child 调一次)
是在onLayoutChildren阶段开始调用。到此第一层缓存的用法已经很明确了,当recyclerView触发onLayout的时候会将所有的View判断塞入mAttachedScrap或者mChangedScrap,再从缓存中取出。如果没变化的ViewHolder直接从mAttachedScrap取出,变化的从mChangedScrap取出在rebind新数据。落实到具体的业务有以下几种情况:
- 调用notifyItemInserted / notifyItemRemoved
比如列表有 10 个 item,在第 3 个位置插入/删除一条新数据。LayoutManager在onLayoutChildren 开始时,把屏幕上 10 个 child 全部 detach 进 Scrap。然后重新填充布局时,第 1、2、4 - 10 的位置都能从 mAttachedScrap 中命中,直接 attach 。第 3 个位置是新 itemScrap里没有,走 createViewHolder + bindViewHolder。
- 调用notifyItemChanged
还是一样的例子,列表有 10 个 item,在第 3 个位置修改一条新数据。LayoutManager在onLayoutChildren开始时,把屏幕上 10 个 child 全部 detach 进 Scrap。其中位置 3 因为数据修改进入mChangedScrap,其他进入mAttachedScrap。但是在绑定阶段,位置3会进行rebind。
这也是RecyclerView局部刷新效率高的原因。
总结一下
mAttachedScrap(未变化的 ViewHolder) ——layout 期间 detach,下一轮 layout 直接 attach 回去,不 rebind,因为数据没变,View 也没变。
mChangedScrap(数据变化的 ViewHolder) ——layout 期间 detach,只在 pre-layout 阶段会被查找,取出来后 rebind 新数据,目的是让 LayoutManager 能测量新内容的尺寸来算动画。到了真正的 layout(step2),这个 ViewHolder 会被正常走一遍完整的 bind 流程。
离屏缓存 (mCachedViews)
当列表从屏幕中划出的时候会调用方法recycleViewHolderInternal
void recycleViewHolderInternal(ViewHolder holder) {
// ... 省略前置检查 ...
// 检查是否可以被缓存
boolean cached = false;
boolean recyclable = false;
if (!holder.hasAnyOfTheFlags(
ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED
| ViewHolder.FLAG_UPDATE)) {
int viewType = holder.getItemViewType();
final int transientState = holder.itemView.getTransientStateType();
// 尝试放入 mCachedViews
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) {
// 淘汰第 0 个(最旧的),将其移入 RecycledViewPool
recycleCachedViewAt(0);
cachedViewSize--;
}
int targetCacheIndex = cachedViewSize;
// 如果开启了预取(prefetch),会根据时间戳决定插入位置
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
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
}
if (!cached) {
// CachedViews 放不下 → 放入 RecycledViewPool
addViewHolderToRecycledViewPool(holder, ...);
}
}
可以看到符合条件的holder会放到mCachedViews中。
此外DEFAULT_CACHE_SIZE 可以知道 mCachedViews 的默认大小为2,我们也可以改变这个配置
int mViewCacheMax = DEFAULT_CACHE_SIZE; // 默认size为2
// RecyclerView.java
public void setItemViewCacheSize(int size) {
mRecycler.setViewCacheSize(size);
}
public void setViewCacheSize(int viewCount) {
mRequestedCacheMax = viewCount;
updateViewCacheSize();
}
void updateViewCacheSize() {
int extraCache = mLayout != null ? mLayout.mPrefetchMaxCountObserved : 0;
mViewCacheMax = mRequestedCacheMax + extraCache;
// first, try the views that can be recycled
for (int i = mCachedViews.size() - 1;
i >= 0 && mCachedViews.size() > mViewCacheMax; i--) {
recycleCachedViewAt(i);
}
}
大体流程如下
用户滑动
→ scrollBy()
→ fill()
→ recycleBy() // 清理旧 item
→ canViewBeRecycled(child) // 检查是否滑出可见区域
→ recycler.recycleView(child) // 回收
→ recycleViewHolderInternal(holder)
→ mCachedViews.add(holder) // 放入缓存
→ layoutChunk() // 填充新 item
→ tryGetViewHolderForPositionByDeadline()
→ 从 mCachedViews 中查找 // 可能命中刚缓存的
自定义缓存 (mViewCacheExtension)
我们可以通过 setViewCacheExtension 设置自定义缓存
void setViewCacheExtension(ViewCacheExtension extension) {
mViewCacheExtension = extension;
}
在方法 tryGetViewHolderForPositionByDeadline 中查找
// tryGetViewHolderForPositionByDeadline 中
if (holder == null) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
if (holder != null) {
// stable id 命中
}
// ← ViewCacheExtension 在这里,排在 Scrap 和 CachedViews 之后、Pool 之前
if (holder == null && mViewCacheExtension != null) {
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view); // 从 View 反查 ViewHolder
if (holder != null) {
// 找到了,但后续会走 rebind
} else {
// View 没有关联的 ViewHolder,不能用
}
}
}
}
注意,这里查找到的是View而不是ViewHolder,用户需要自己解决缓存的问题。简单来说,如果要使用自定义缓存,则需要开发者自己解决如何存和如何取这两个关键问题,下面给出一个简单的案例:
第一步
class MyCacheExtension extends RecyclerView.ViewCacheExtension {
// 你自己维护一个容器
private List<View> myCache = new ArrayList<>();
@Override
public View getViewForPositionAndType(Recycler recycler, int position, int type) {
if (type == TYPE_VIDEO && !myCache.isEmpty()) {
return myCache.remove(0); // 有就返回,没有就返回 null
}
return null; // 返回 null 表示"我这里没有,你走默认流程"
}
}
第二步
class MyRecyclerListener implements RecyclerView.RecyclerListener {
private MyCacheExtension extension;
MyRecyclerListener(MyCacheExtension ext) {
this.extension = ext;
}
@Override
public void onViewRecycled(RecyclerView.ViewHolder holder) {
if (holder.getItemViewType() == TYPE_VIDEO) {
extension.myCache.add(holder.itemView); // 手动存进去
}
}
}
第三步
MyCacheExtension extension = new MyCacheExtension();
recyclerView.setViewCacheExtension(extension);
recyclerView.setRecyclerListener(new MyRecyclerListener(extension));
要注意的是,自定义缓存还是会走onBindViewHolder的。自定义缓存通常用于View的复用,onBindViewHolder还是不要执行繁琐的操作。
缓存池(RecyclerViewPool)
RecycledViewPool 是四级缓存里最"重"的一级,也是唯一支持跨 RecyclerView 共享的缓存。
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; // onCreateViewHolder 平均耗时
long mBindRunningAverageNs = 0; // onBindViewHolder 平均耗时
}
SparseArray<ScrapData> mScrap = new SparseArray<>(); // key = viewType
private int mAttachCount = 0; // 有多少个 RecyclerView 挂在上面
}
RecyclerViewPool内部利用了SparseArray对ViewHolder进行缓存。内部按 viewType 分组,每种 viewType 对应一个 ScrapData,每个 ScrapData 内部是一个 ArrayList,默认最多存 5 个 ViewHolder。SparseArray 比 HashMap 更省内存,适合 key 是 int 的场景。
数据存入流程:
public void putRecycledView(ViewHolder scrap) {
final int viewType = scrap.getItemViewType();
final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
// 超过上限return
if (mMaxScrap >= scrapHeap.size()) {
return;
}
// 防御检查:同一个 ViewHolder 不能重复放入
if (scrapHeap.contains(scrap)) {
throw new IllegalArgumentException("this scrap item already exists");
}
scrap.resetInternal(); // 重置 ViewHolder 状态
scrap.mOwnerRecyclerView = null;
scrap.mBindingAdapter = null;
scrapHeap.add(scrap); // 追加到尾部
}
重点关注一下scrap.resetInternal()方法
void resetInternal() {
if (sDebugAssertionsEnabled && isTmpDetached()) {
throw new IllegalStateException("Attempting to reset temp-detached ViewHolder: "
+ this + ". ViewHolders should be fully detached before resetting.");
}
// 清空所有的标记,position,id
mFlags = 0;
mPosition = NO_POSITION;
mOldPosition = NO_POSITION;
mItemId = NO_ID;
mPreLayoutPosition = NO_POSITION;
mIsRecyclableCount = 0;
mShadowedHolder = null;
mShadowingHolder = null;
clearPayload();
mWasImportantForAccessibilityBeforeHidden = View.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
mPendingAccessibilityState = PENDING_ACCESSIBILITY_STATE_NOT_SET;
clearNestedRecyclerViewIfNotNested(this);
}
这就是为什么从 Pool 取出的 ViewHolder 必须 重新bind。
@Nullable
public ViewHolder getRecycledView(int viewType) {
final ScrapData scrapData = mScrap.get(viewType);// 根据ViewType取出scrapData
if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
for (int i = scrapHeap.size() - 1; i >= 0; i--) { // 先去最后的ViewHolder
if (!scrapHeap.get(i).isAttachedToTransitionOverlay()) {
return scrapHeap.remove(i);
}
}
}
return null;
}
因为最后放入 Pool 的 ViewHolder,它的 View 可能刚刚还在屏幕上渲染过,复用它的渲染成本更低。
总结: RecycledViewPool 实际上是根据ViewType缓存ViewHolder的,减少重新创建View的过程。