文章目录
一、概述
我们知道 RecyclerView 其实是继承 ViewGroup 实现的,也就是说它的最根本的功能是可以往 RecyclerView 中添加子 View。如往 LinearLayout 中添加子 View,子 View 就会横向或纵向展开排列。以 LinearLayout 纵向排列为例,当子 View 过多时,超出屏幕部分的子 View 就显示不出来,这时引入一种 滑动机制 来确保所有子 View 都可以展示 (通过 Scroller/ OverScroller 类辅助实现滑动效果),类似 ScrollerView。当子 View 数量很多时,又会产生另一个新问题 (内存占用大:因为所有子 View 需要同时存在)。但是这种场景下有个特点,即屏幕展示的子 View 有限,大部分子 View 都不可见。所以新增了 View 的复用机制,即通过回收不可见的子 View 并在展示新 View 时进行复用来实现优化。这便是 ListView、RecyclerView 等存在的意义。
版本: Android SDK 29
关联文章:
RecyclerView 的关联模块:
- LayoutManager:负责 RecyclerView 中,控制 item 的布局方向
- Recycler:负责View的缓存。
- RecyclerView.Adapter:为 RecyclerView 承载数据
- ItemDecoration:为 RecyclerView 添加分割线
- ItemAnimator:控制 RecyclerView 中 item 的动画
前面我们刚刚分析了RecyclerView 三大绘制流程,接下来我们重点分析缓存原理。
二、缓存的分类
Recycler 负责管理废弃或被 detached 的 item 视图,以便重复利用。先来看下他的四级缓存:
缓存级别 | 缓存 | 作用 |
---|---|---|
一级缓存 | mAttachedScrap、mChangedScrap | 不参与滑动时的回收复用,仅作为重新布局时的一种临时缓存。 |
二级缓存 | mCachedViews | 是一个 ViewHolder 集合,负责对刚刚移出屏幕的View进行回收复用的缓存列表。 |
三级缓存 | ViewCacheExtension | 用户自定义的缓存 (一般不使用) |
四级缓存 | RecycledViewPool | ViewHolder 缓存池,可以实现多个RecyclerView公用。以viewType为key可以缓存多个同类型的ViewHolder。 |
2.1 Scrap 缓存
Scrap 是 RecyclerView 中最轻量的缓存,它不参与滑动时的回收复用,仅作为重新布局时的一种临时缓存。目的是缓存当界面重新布局的前后都出现在屏幕上的ViewHolder,以此省去不必要的重新加载与绑定工作。
- mAttachedScrap:负责保存将会原封不动的ViewHolder。
- mChangedScrap:负责保存位置会发生移动的ViewHolder,注意只是位置发生移动,内容仍原封不动。
public final class Recycler {
// 负责保存将会原封不动的ViewHolder。
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
// 负责保存位置会发生移动的ViewHolder,注意只是位置发生移动,内容仍原封不动。
ArrayList<ViewHolder> mChangedScrap = null;
}
上面两个 Scrap 缓存的实际应用场景如下图所示:(图片来源 RecyclerView源码解析)
2.2 CachedViews 缓存
CacheView 是以 ViewHolder 为单位,负责在 RecyclerView 列表位置产生变动的时候,对刚刚移出屏幕的 View 进行回收复用的缓存列表。
public final class Recycler {
// 缓存刚刚移除屏幕的ViewHolder。
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
private int mRequestedCacheMax = DEFAULT_CACHE_SIZE; //DEFAULT_CACHE_SIZE=2
int mViewCacheMax = DEFAULT_CACHE_SIZE;
}
上面 CachedViews 缓存的实际应用场景如下图所示:(图片来源 RecyclerView源码解析)
2.3 ViewCacheExtension 缓存
ViewCacheExtension 是一个由用户自定义的缓存,其定义如下:
public abstract static class ViewCacheExtension {
public abstract View getViewForPositionAndType(
@NonNull Recycler recycler, int position, int type);
}
小结:
- RecyclerView 调用缓存的顺序, Scrap缓存 -> CacheViews 缓存 -> ViewCacheExtension 缓存 -> RecyclerViewPool 缓存。
- ViewCacheExtension 缓存只有取缓存的接口,但是 Recycler 没有对应添加缓存的接口,因此需要自己找场景插入添加缓存的逻辑。
2.4 RecycledViewPool 缓存
在 Srap 和 CacheView 不愿意缓存的时候,都会放入 RecycledViewPool 进行回收。同时,RecycledViewPool 只按照 ViewType 进行区分,只要 ViewType 是相同的,甚至可以在多个 RecyclerView 中进行通用的复用,只要为它们设置同一个RecycledViewPool 就可以了。
public static class RecycledViewPool {
// 默认最多缓存5个。
private static final int DEFAULT_MAX_SCRAP = 5;
SparseArray<ScrapData> mScrap = new SparseArray<>();
static class ScrapData {
final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
long mCreateRunningAverageNs = 0;
long mBindRunningAverageNs = 0;
}
}
RecycledViewPool 的数据结构如下图所示,可以缓存 viewType 相同的多个 ViewHolder。
三、缓存的操作 (存/取)
上面介绍完了缓存的分类及其特点之后,下面我们来看看这个类型的缓存在哪些地方存取的。
3.1 缓存的回收
Condition1:Scrap 回收
在 《RecyclerView(一) — 绘制流程分析》 中我们知道,在调用LayoutManager.onLayoutChildren() 方法时触发了 Scrap 的缓存。
下面我们就来分析一下这个缓存的流程。
// LayoutManager.class
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
detachAndScrapAttachedViews(recycler);
}
public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
final int childCount = getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
final View v = getChildAt(i);
scrapOrRecycleView(recycler, i, v);
}
}
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
// 获取每个Item对应的ViewHolder。
final ViewHolder viewHolder = getChildViewHolderInt(view);
if (viewHolder.shouldIgnore()) {
return;
}
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {
removeViewAt(index);
recycler.recycleViewHolderInternal(viewHolder);
} else {
detachViewAt(index);
// 最终在这里会缓存到对应的Scrap缓存中。
recycler.scrapView(view);
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
}
}
Condition2:滑动中的 ViewHolder 回收
下面介绍在滑动中回收 ViewHolder 的流程。
// LayoutManager.class
// 调用链:
//fill()
//->recycleByLayoutState()
//->recycleViewsFromStart()
//->recycleChildren()
//->LayoutManager.removeAndRecycleViewAt()
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
// View 的回收逻辑
recycleByLayoutState(recycler, layoutState);
}
private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
if (!layoutState.mRecycle || layoutState.mInfinite) {
return;
}
int scrollingOffset = layoutState.mScrollingOffset;
int noRecycleSpace = layoutState.mNoRecycleSpace;
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
// 从End端开始回收视图
recycleViewsFromEnd(recycler, scrollingOffset, noRecycleSpace);
} else {
// 从Start端开始回收视图
// 调用链:recycleViewsFromStart()->recycleChildren()->LayoutManager.removeAndRecycleViewAt()
recycleViewsFromStart(recycler, scrollingOffset, noRecycleSpace);
}
}
public void removeAndRecycleViewAt(int index, @NonNull Recycler recycler) {
final View view = getChildAt(index);
removeViewAt(index);
recycler.recycleView(view);
}
RecycleView.recycleView()
// RecycleView.class
public void recycleView(@NonNull View view) {
ViewHolder holder = getChildViewHolderInt(view);
//...清除状态...
// 1.回收ViewHolder
recycleViewHolderInternal(holder);
//...
}
void recycleViewHolderInternal(ViewHolder holder) {
//...各种无法回收逻辑的判断...
boolean cached = false;
boolean recycled = false;
if (forceRecycle || holder.isRecyclable()) {
if (mViewCacheMax > 0
&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_REMOVED
| ViewHolder.FLAG_UPDATE
| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
// Retire oldest cached view
// 滑动的视图,先保存在mCachedViews中
int cachedViewSize = mCachedViews.size();
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
// mCachedViews只能缓存mViewCacheMax个,如果保存不下,
// 就将mCachedViews中最久的那个移到RecycledViewPool。
recycleCachedViewAt(0);
cachedViewSize--;
}
int targetCacheIndex = cachedViewSize;
if (ALLOW_THREAD_GAP_WORK
&& cachedViewSize > 0
&& !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
// when adding the view, skip past most recently prefetched views
int cacheIndex = cachedViewSize - 1;
while (cacheIndex >= 0) {
int cachedPos = mCachedViews.get(cacheIndex).mPosition;
if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
break;
}
cacheIndex--;
}
targetCacheIndex = cacheIndex + 1;
}
// 将本次回收的ViewHolder放到mCachedViews中.
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
if (!cached) {
// 如果没能缓存到CacheViews里,则缓存到RecycledViewPool。
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
} else {
//
}
// even if the holder is not removed, we still call this method so that it is removed
// from view holder lists.
mViewInfoStore.removeViewHolder(holder);
if (!cached && !recycled && transientStatePreventsRecycling) {
holder.mOwnerRecyclerView = null;
}
}
小结:
滑动时的View回收的过程:
- 判断是否满足回收条件,如果不满足,则直接抛出异常。
- 满足回收条件 & 满足缓存到 CachedViews 的条件都成立。
- 如果CachedViews 条数未达到最大值,则直接缓存 ViewHolder。
- 如果CachedViews 条数已经达到最大值,则将 CachedViews 中最老的一个 ViewHolder 移到 RecycledViewPool 中,然后缓存ViewHolder。
- 如果 CachedViews 没有缓存成功,则直接将 ViewHolder 缓存到 RecycledViewPool 。当 RecycledViewPool 中ViewType 对应的ViewHolder缓存数量达到最大值时,则不再缓存。
3.2 缓存的获取
// LayoutManager.class
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
View view = layoutState.next(recycler);
//...
}
// LayoutState.class
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
// 最终调用 Recycler.tryGetViewHolderForPositionByDeadline()方法
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
// Recycler.class
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
// ...
boolean fromScrapOrHiddenOrCache = false;
ViewHolder holder = null;
// 0) If there is a changed scrap, try to find from there
// 1.先从mChangedScrap中获取。当数据位置发生变化的时候,会走这个逻辑。
// 例如:notifyItemRemove()后,下面的数据会上移,会走这个逻辑。
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
// 1) Find by position from scrap/hidden list/cache
if (holder == null) {
// 2.根据position依次从mAttachedScrap、隐藏的列表、mCachedViews中获取。
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if (holder != null) {
// 检验获取的holder是否合法。
// 不合法:就会将holder进行回收。
// 合法:则标记fromScrapOrHiddenOrCache为true。表明holder是从这缓存中获取的。
if (!validateViewHolderForOffsetPosition(holder)) {
if (!dryRun) {
holder.addFlags(ViewHolder.FLAG_INVALID);
if (holder.isScrap()) {
removeDetachedView(holder.itemView, false);
holder.unScrap();
} else if (holder.wasReturnedFromScrap()) {
holder.clearReturnedFromScrapFlag();
}
recycleViewHolderInternal(holder);
}
holder = null;
} else {
fromScrapOrHiddenOrCache = true;
}
}
}
if (holder == null) {
//...
// 2) Find from scrap/cache via stable ids, if exists
if (mAdapter.hasStableIds()) {
// 3.根据id依次尝试从 mAttachedScrap、mCachedViews中获取。
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
if (holder != null) {
// update position
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
}
}
if (holder == null && mViewCacheExtension != null) {
// We are NOT sending the offsetPosition because LayoutManager does not
// know it.
// 4.尝试从我们自定义的 mViewCacheExtension中去获取。
final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
if (holder == null || holder.shouldIgnore()) {
throw new IllegalArgumentException("msg");
}
}
}
if (holder == null) { // fallback to pool
// 5.从RecycledViewPool缓存池里面获取。
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
if (holder == null) {
long start = getNanoTime();
if (deadlineNs != FOREVER_NS
&& !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
// abort - we have a deadline we can't meet
return null;
}
// 6.缓存中找不到,就创建新的ViewHolder。
holder = mAdapter.createViewHolder(RecyclerView.this, type);
if (ALLOW_THREAD_GAP_WORK) {
// only bother finding nested RV if prefetching
RecyclerView innerView = findNestedRecyclerView(holder.itemView);
if (innerView != null) {
holder.mNestedRecyclerView = new WeakReference<>(innerView);
}
}
long end = getNanoTime();
mRecyclerPool.factorInCreateTime(type, end - start);
}
}
//...
boolean bound = false;
if (mState.isPreLayout() && holder.isBound()) {
// do not update unless we absolutely have to.
// 数据不需要绑定(一般从mChangedScrap,mAttachedScrap中得到的缓存Holder是不需要进行重新绑定的)
holder.mPreLayoutPosition = position;
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
// 调用Adapter.bingdViewHolder()
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
//...
return holder;
}
小结:
获取缓存的流程:
- 从 mChangedScrap 中获取去获取。
- 根据 position 依次尝试从 mAttachedScrap、隐藏的列表、mCachedViews 中获取。
- 根据 id 依次尝试从 mAttachedScrap、mCachedViews 中获取。
- 尝试从我们自定义的 mViewCacheExtension 中去获取。
- 根据 ViewType 从缓存池里面获取。
- 如果上面都无法获取的话就通过
Adapter.createViewHolder()
来创建ViewHolder。
四、RecyclerView 与 ListView 的区别
ListView:2级缓存
RecyclerView:4级缓存
ListView 和 RecyclerView 缓存机制基本一致:
- mActiveViews 和 mAttachedScrap 功能相似,目的在于快速重用屏幕上可见的列表项 ItemView,而不需要重新 createView 和 bindView。
- mScrapView 和 mCachedViews + mReyclerViewPool 功能相似,目的在于缓存离开屏幕的 ItemView,目的是让即将进入屏幕的ItemView重用。
- RecyclerView 的优势在于 mCacheViews 的使用,可以做到屏幕外的列表项 ItemView 进入屏幕内时也无须 bindView,可以快速重用。同时,RecyclerViewPool 可以供多个RecyclerView共同使用。
缓存层级不同:
- ListView 2级缓存,RecyclerView 4级缓存,且RecyclerViewPool 支持多个 RecyclerView 共享。
缓存形式不同:
- RecyclerView 缓存的是 ViewHolder (即 View + ViewHolder +Flag 标识状态),避免每次 createView 时调用findViewById。
- ListView缓存View,所以每次从缓存获取时需要重新 bindView。
局部刷新:
- 在ListView 中刷新数据通常使用 notifyDataSetChanged() 进行全局刷新,非常消耗资源。
- RecyclerView 中可以实现局部刷新。ListView 的局部刷新效果需要自己实现。
动画效果:
- 在RecyclerView 中支持动画效果的接口,我们只需要实现自定义的动画效果即可。
- ListView 需要手动在 Adapter 中实现动画效果。