前言
谈到 RecyclerView,大家肯定都不陌生,基本上每个 Android APP 都会用到,它是 Android 中一个非常重要的组件,用于灵活、高效的展示大量数据集合。本文将通过 RecyclerView 的源码简单分析它的缓存复用机制。
下面开始。
1、RecyclerView 缓存和复用的基本知识
在了解缓存复用机制之前,先了解 RecyclerView 中的一个内部类:Recycler,这个类是 RecycelerView 缓存复用机制的核心。下面我们看看 Recycler 中的关于缓存复用机制的成员变量:
Java
public final class Recycler {
final ArrayList<RecyclerView.ViewHolder> mAttachedScrap = new ArrayList<>();
ArrayList<RecyclerView.ViewHolder> mChangedScrap = null;
final ArrayList<RecyclerView.ViewHolder> mCachedViews = new ArrayList<RecyclerView.ViewHolder>();
RecyclerView.RecycledViewPool mRecyclerPool;
private RecyclerView.ViewCacheExtension mViewCacheExtension;
}
从上述代码可以看出,缓存复用的对象是 ViewHolder。上面五个成员变量就是我们常说的 RecyclerView 的四级缓存,分别是:
mChangedScrap
与mAttachedScrap
,用来缓存还在屏幕内的 ViewHolder。mCachedViews
,用来缓存移出屏幕之外的 ViewHolder。mViewCacheExtension
,这一层的创建和缓存完全由开发者自己控制,初始值为null
。mRecyclerPool
,ViewHolder 缓存池。
2、RecyclerView 复用流程分析
RecyclerView 的复用流程执行在 Recycler 的 tryGetViewHolderForPositionByDeadline()
方法中,来看源码中怎么执行复用操作:
Java
tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
......
......
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
......
......
}
if (holder == null) {
......
......
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
......
......
}
if (holder == null && mViewCacheExtension != null) {
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
......
......
}
}
if (holder == null) { // fallback to pool
......
holder = getRecycledViewPool().getRecycledView(type);
......
......
}
if (holder == null) {
......
holder = mAdapter.createViewHolder(RecyclerView.this, type);
......
......
}
}
......
......
boolean bound = false;
if (mState.isPreLayout() && holder.isBound()) {
holder.mPreLayoutPosition = position;
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
......
......
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
......
......
return holder;
}
从上面的流程我们可以看到,这些 if
语句都会去判断 holder
是否已经从缓存中拿到并复用了,如果已经拿到,则后面的 if
语句都进不去,则运行到后续并返回 holder
,否则 holder == null
,则进行下一层的从缓存中复用。
由上到下按层级依次是:
getChangedScrapViewForPosition()
方法,根据 position 和 stableId 从mChangedScrap
中查找。getScrapOrHiddenOrCachedHolderForPosition()
方法,根据 position 从mAttachedScrap
和mCachedViews
中查找。getScrapOrCachedViewForId()
方法,根据 stableId 从mAttachedScrap
和mCachedViews
中查找。mViewCacheExtension.getViewForPositionAndType()
方法,根据自定义实现的缓存和复用规则运行,没有自定义则不会进入这步。getRecycledViewPool().getRecycledView()
方法,从缓存池中查找。
下面是 tryGetViewHolderForPositionByDeadline()
方法中的后续步骤:
如果上述的所有步骤都没有复用到 ViewHolder,则调用 mAdapter.createViewHolder()
方法直接新建一个 ViewHolder,此时调用到的就是 adapter 中的 onCreateViewHolder()
方法。
上述所有过程运行结束之后,此时不论是通过复用还是新建,holder 都已经不为 null 了,则会调用 tryBindViewHolderByDeadline()
方法进行数据处理,adapter 的 onBindViewHolder()
方法就是在这里调用到的。
上述各个方法具体的细节就不展开分析,有兴趣或需求的可以自己到源码里看看。
3、RecyclerView 缓存原理分析
上面提到了复用分了四层,而复用的 ViewHolder 对象就是从各层缓存中查找的,下面我们来分析缓存的机制。
mChangedScrap
与 mAttachedScrap
缓存
这层的缓存在 Recycler 的 scrapView()
方法中执行:
Java
void scrapView(View view) {
final RecyclerView.ViewHolder holder = getChildViewHolderInt(view);
if (holder.hasAnyOfTheFlags(RecyclerView.ViewHolder.FLAG_REMOVED | RecyclerView.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());
}
holder.setScrapContainer(this, false);
mAttachedScrap.add(holder);
} else {
if (mChangedScrap == null) {
mChangedScrap = new ArrayList<RecyclerView.ViewHolder>();
}
holder.setScrapContainer(this, true);
mChangedScrap.add(holder);
}
}
这个方法主要用于缓存出现在屏幕内的 item,当我们调用了 adapter 的 notifyXXX()
方法通知 item 发生变化后,会使用 mAttachedScrap
缓存没有发生变化的 ViewHolder,其他的则由 mChangedScrap
缓存,添加 itemView 的时候快速从里面取出,完成局部刷新。
mCachedViews
与 mRecyclerPool
缓存
这两层缓存主要是在 Recycler 的 recycleView()
方法中:
Java
public void recycleView(@NonNull View view) {
ViewHolder holder = getChildViewHolderInt(view);
if (holder.isTmpDetached()) {
removeDetachedView(view, false);
}
if (holder.isScrap()) {
holder.unScrap();
} else if (holder.wasReturnedFromScrap()) {
holder.clearReturnedFromScrapFlag();
}
recycleViewHolderInternal(holder);
if (mItemAnimator != null && !holder.isRecyclable()) {
mItemAnimator.endAnimation(holder);
}
}
其中的 recycleViewHolderInternal()
方法是关键,里面处理了 mCachedViews
和 mRecyclerPool
的缓存,继续点进去看源码:
Java
void recycleViewHolderInternal(RecyclerView.ViewHolder holder) {
......
......
if (forceRecycle || holder.isRecyclable()) {
if (mViewCacheMax > 0
&& !holder.hasAnyOfTheFlags(RecyclerView.ViewHolder.FLAG_INVALID
| RecyclerView.ViewHolder.FLAG_REMOVED
| RecyclerView.ViewHolder.FLAG_UPDATE
| RecyclerView.ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
int cachedViewSize = mCachedViews.size();
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
recycleCachedViewAt(0);
cachedViewSize--;
}
......
......
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
if (!cached) {
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
} else {
......
......
}
......
......
}
进入第一个 if
之后,会先判断 mViewCacheMax
是否大于 0,这个 mViewCacheMax
就是 mCachedViews
能保存的最大容量,默认是 2,因此这个条件满足,&&
后面的条件主要是判断当前 ViewHolder 的状态是否 不
处于"失效、移除、更新、未知"这四个状态,即已经移出屏幕,如果不满足这个条件,则直接调用 addViewHolderToRecycledViewPool()
方法将 ViewHolder 存入缓存池,否则,会继续判断当前 mCachedViews
是否已满,下面分两种情况:
- 已满
调用recycleCachedViewAt()
方法:
Java
void recycleCachedViewAt(int cachedViewIndex) {
if (DEBUG) {
Log.d(TAG, "Recycling cached view at index " + cachedViewIndex);
}
RecyclerView.ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
if (DEBUG) {
Log.d(TAG, "CachedViewHolder to be recycled: " + viewHolder);
}
addViewHolderToRecycledViewPool(viewHolder, true);
mCachedViews.remove(cachedViewIndex);
}
这里执行的流程是: 取出 mCachedViews
中第一个 ViewHolder,再调用上面提到过的 addViewHolderToRecycledViewPool()
方法将 ViewHolder 存入缓存池,最后从 mCachedViews
中移除掉这个 ViewHolder。
执行完上述流程后,mCachedViews
便有一个空闲位置,之后调用 mCachedViews.add()
方法顺利将需要回收的 ViewHolder 保存到 mCachedViews
中。
- 未满
未满则跳过了if
,直接调用mCachedViews.add()
方法将需要回收的 ViewHolder 保存到mCachedViews
中。
因此我们知道了:mCachedViews
中保存的 ViewHolder 一定是最近移出屏幕的,稍早移出屏幕的 ViewHolder 先被保存到 mCachedViews
,随后被转存到缓存池中。
另外,还有一层 mViewCacheExtension
缓存,由于作者水平有限,开发过程中还没用到过,这里就不多说。
最后补充一个 RecyclerView 使用过程中的操作:
如果在 RecyclerView 滑动过程中出现 View 的闪烁或者数据绑定错乱,可以调用 adapter 的 setHasStableIds()
,并传入参数 true
,此方法告诉 RecyclerView 的 Adapter 每个 item 的 id 是否固定不变的,hasStableIds 为 true 时,意味着相同的 id 总是指向相同的数据项,每个数据项的 id 在 RecyclerView 的生命周期内应该是唯一且固定的,RecyclerView 可以使用这些 id 来确定哪些项目保持不变,从而实现更有效的视图缓存和复用策略,特别是在数据集发生变化时。这有助于减少不必要的视图创建和绑定,显著提升滑动和更新时的性能。
👉 以上就是对 RecyclerView 缓存复用机制的简单分析。