缓存——RecyclerView源码详解
Posted 薛瑄
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了缓存——RecyclerView源码详解相关的知识,希望对你有一定的参考价值。
刚才有个朋友问我,博主发生什么事了,给我发了几张截图,我一看,哦,原来是有个大帅哔看了文章,说是,博主,我能白嫖你的文章,我说年轻人,点个赞再走,他说不点,我说点一个,他说不点,我说点一个,他说不点,我说我这文章对你有用,他不服气,说要先看看。我说可以,很快啊,看完后,就是一个复制,一个粘贴,一个网页关闭,我大意了啊,没有删除文章。按传统博客的有用为止,他说已经输了啊。 后来他说他是乱点的,这可不是乱点的啊,训练有素。我劝年轻人好好点赞,耗子尾汁,谢谢朋友们
前言
整体流程、measure、layout 详解 ——深入分析RecyclerView源码(一)
缓存 ——深入分析RecyclerView源码(二)
这篇文章,分析一下RecycleView 的缓存机制,分为两大部分: 从缓存获取View和 把View保存到缓存中
缓存的核心是交给 Recycler 类来处理的,包括存储缓存,获取缓存等。缓存的数据类型是ViewHold,它包含了itemView,mPosition 等Item的信息
从缓存获取ViewHold
先来了解Recycler中五个变量,每个变量都与缓存相关,按照缓存获取的顺序来逐一介绍
一级缓存:返回布局和内容都都有效的ViewHolder
- 按照position或者id进行匹配
- 命中一级缓存无需onCreateViewHolder和onBindViewHolder
-
mAttachedScrap ArrayList : 未与RecyclerView分离的ViewHolder列表。如果仍依赖于 RecyclerView (比如已经滑动出可视范围,但还没有被移除掉),但已经被标记移除的 ItemView 集合会被添加到 mAttachedScrap 中按照id和position来查找ViewHolder。在每次绘制RecycleView的时候,都会先把界面上的ViewHolder收集到mAttachedScrap,然后在绘制的时候,方便复用
-
mChangedScrap ArrayList:表示数据已经改变的viewHolder列表,存储 notifXXX 方法时需要改变的 ViewHolder,匹配机制按照position和id进行匹配
-
mCachedViews ArrayList:缓存ViewHolder,主要用于解决RecyclerView滑动抖动时的情况,还有用于保存Prefetch的ViewHoder。
- 位置相同的ViewHolder,才能复用。复用的ViewHolder,不需要bindViewHolder,可直接拿去绘制。
- 最大的数量为:mViewCacheMax = mRequestedCacheMax + extraCache(extraCache是由prefetch的时候计算出来的)
二级缓存:返回View
- 按照position和type进行匹配
- 直接返回View
- mViewCacheExtension ViewCacheExtension:开发者可自定义的一层缓存,是虚拟类ViewCacheExtension的一个实例,开发者需实现方法
getViewForPositionAndType(Recycler recycler, int position, int type)
,注意它返回的是View。
三级缓存:返回布局有效,内容无效的ViewHolder
- layout是有效的,但是内容是无效的,需要调用bindViewHolder
- 多个RecycleView可共享,可用于多个RecyclerView的优化
- mRecyclerPool ViewHolder缓存池。 当mCachedViews缓存数量达到上限,就会把mCachedViews中的一个ViewHolder存入RecyclerViewPool中,把当前的ViewHolder存入mCachedViews。
- 按照Type来查找ViewHolder
- 每个Type默认最多缓存5个。
这里我们在上一篇文章的layoutChunk 函数开始分析,获取一个View,然后进行布局。
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result)
// 获取View,如果缓存中没有,就新创建
View view = layoutState.next(recycler);
//省略代码
在LayoutState类中
//返回下一个位置的View
View next(RecyclerView.Recycler recycler)
if (mScrapList != null)
return nextViewFromScrapList();
//通过Recycle,来获取View
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
recycler.getViewForPosition(mCurrentPosition);
最终会调用到 tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS)
后者返回的是ViewHolder。下面就来分析一下这个函数,它是获取缓存的重点函数
在Recycle类中
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
if (mState.isPreLayout())
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
// 1) Find by position from scrap/hidden list/cache
if (holder == null)
// 先从mAttachedScrap、Hidden、mCachedViews中 查找,位置是position 的ViewHolder
//这里获取到指定position的缓存,该类型的缓存,可直接复用,不需要重新onBindViewHolder
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if (holder != null)
//检查ViewHolder 是否可用;位置是否正确;itemViewType是否匹配;若item有固定id(Stable Id),该id是否正确
if (!validateViewHolderForOffsetPosition(holder))
// recycle holder (and unscrap if relevant) since it can't be used
if (!dryRun)
// we would like to recycle this but need to make sure it is not used by
// animation logic etc.
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;
//判断是否获取到了缓存的ViewHolder,如果为空,就在mCachedViews 中进行获取
if (holder == null)
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
//判断 是否合法
if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount())
throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
+ "position " + position + "(offset:" + offsetPosition + ")."
+ "state:" + mState.getItemCount() + exceptionLabel());
final int type = mAdapter.getItemViewType(offsetPosition);
// 2) Find from scrap/cache via stable ids, if exists
if (mAdapter.hasStableIds())
//如果存在固定id,则去mAttachedScrap、mCachedViews 中去查找 是否有对应的ViewHolder
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
if (holder != null)
// update position
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
// 还没有获取到缓存 ViewHolder,就去mViewCacheExtension 去获取,这个是自定义的缓存,可能为空
if (holder == null && mViewCacheExtension != null)
// We are NOT sending the offsetPosition because LayoutManager does not
// know it.
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
if (view != null)
holder = getChildViewHolder(view);
。。。省略代码。。。
//还是没有获取到缓存,就去RecycledViewPool 按照itemView Type 查找,
//找到的缓存,因为可能位置可能,所以需要重新onBinderViewHolder 的,
if (holder == null) // fallback to pool
。。。省略代码。。。
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null)
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST)
invalidateDisplayListInt(holder);
//到此,所有的缓存都查找过了,还是没有的话,就去createViewHolder 创建一个新的ViewHolder
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;
//这个函数,大家应该很熟悉了
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.
holder.mPreLayoutPosition = position;
else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid())
// 根据holder的mFlags 标志,来判断当前ViewHolder的状态。mFlags 是Int 类型,每个二进制位,表示一种状态
//mFlags 默认是0 ,也就是任何状态都是 0
//在下面的缓存回收机制,可以看到如果有ViewHolder.FLAG_INVALID、ViewHolder.FLAG_UPDATE 这里两种状态
//是不能加入mCachedViews中的,所以也就是说mCachedViews 获取到的缓存是不需要onBindViewHolder
。。。省略代码。。。
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
//调用到onBindViewHolder,来把数据绑定到View
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
//获取ViewHolder 的LayoutParams,
final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
//这个LayoutParams 是RecycleView 自定义的,继承于android.view.ViewGroup.MarginLayoutParams
final LayoutParams rvLayoutParams;
if (lp == null)
rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
holder.itemView.setLayoutParams(rvLayoutParams);
else if (!checkLayoutParams(lp))
rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
holder.itemView.setLayoutParams(rvLayoutParams);
else
rvLayoutParams = (LayoutParams) lp;
rvLayoutParams.mViewHolder = holder;
rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
return holder;
其中涉及到缓存查找的几个函数,例如 getScrapOrHiddenOrCachedHolderForPosition
、getScrapOrCachedViewForId
等,基本思路就是变量相关的缓存变量,来查找符合条件的ViewHolder。 这里就不展开了,下面来看一下ViewHolder 是如何被回收到这些缓存中的
回收ViewHold到缓存
缓存的获取,很简单,按照一定的顺序,在不同的缓存层级查找。回收 才代表缓存内部的逻辑,保存到哪个缓存层级,以及何时被转移。缓存回收有很多中情况,下面我们以 item 滑出屏幕 为例,来分析缓存的回收
先来一张图,对整体的回收逻辑 有个认识
从滑动事件的入口,来分析
在RecycleView 类中
@Override
public boolean onTouchEvent(MotionEvent e)
。。。省略代码。。。
switch (action)
。。。省略代码。。。
//滑动操作,
case MotionEvent.ACTION_MOVE:
。。。省略代码。。。
final int index = e.findPointerIndex(mScrollPointerId);
if (mScrollState == SCROLL_STATE_DRAGGING)
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
//这个是重点
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
vtev))
getParent().requestDisallowInterceptTouchEvent(true);
//调用预布局等操作
if (mGapWorker != null && (dx != 0 || dy != 0))
mGapWorker.postFromTraversal(this, dx, dy);
break;
在RecycleView 类中
boolean scrollByInternal(int x, int y, MotionEvent ev)
int unconsumedX = 0, unconsumedY = 0;
int consumedX = 0, consumedY = 0;
consumePendingUpdateOperations();
if (mAdapter != null)
//重点函数
scrollStep(x, y, mScrollStepConsumed);
consumedX = mScrollStepConsumed[0];
consumedY = mScrollStepConsumed[1];
unconsumedX = x - consumedX;
unconsumedY = y - consumedY;
if (!mItemDecorations.isEmpty())
invalidate();
//处理嵌套滑动
if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
TYPE_TOUCH))
// Update the last touch co-ords, taking any scroll offset into account
mLastTouchX -= mScrollOffset[0];
mLastTouchY -= mScrollOffset[1];
if (ev != null)
ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
else if (getOverScrollMode() != View.OVER_SCROLL_NEVER)
if (ev != null && !MotionEventCompat.isFromSource(ev, InputDevice.SOURCE_MOUSE))
pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY);
considerReleasingGlowsOnScroll(x, y);
if (consumedX != 0 || consumedY != 0)
dispatchOnScrolled(consumedX, consumedY);
if (!awakenScrollBars())
invalidate();
return consumedX != 0 || consumedY != 0;
void scrollStep(int dx, int dy, @Nullable int[] consumed)
//开始拦截 ,避免多余的requestLayout。主要是设置变量mInterceptRequestLayoutDepth,开启拦截就+1,关闭就-1
//如果该值为0,表示该条件 允许绘制
startInterceptRequestLayout();
//表示进入布局或滚动,此时不能改变adapter的数据,否则会抛出异常。mLayoutOrScrollCounter,进入+1,退出-1
onEnterLayoutOrScroll();
TraceCompat.beginSection(TRACE_SCROLL_TAG);
fillRemainingScrollValues(mState);
//在x y 轴上 滑动消耗的值
int consumedX = 0;
int consumedY = 0;
//下面这两个滑动函数,调用到LayoutManager中,
if (dx != 0)
consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
if (dy != 0)
consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
TraceCompat.endSection();
repositionShadowingViews();
//退出布局或滚动
onExitLayoutOrScroll();
//关闭拦截 ,允许的requestLayout
stopInterceptRequestLayout(false);
if (consumed != null)
consumed[0] = consumedX;
consumed[1] = consumedY;
下面来以scrollVerticallyBy 为例,来分析,下面的函数进入了LinearLayoutManager.java 中
在LinearLayoutManager.java中
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
RecyclerView.State state)
if (mOrientation == HORIZONTAL)
return 0;
return scrollBy(dy, recycler, state);
int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state)
if (getChildCount() == 0 || dy == 0)
return 0;
mLayoutState.mRecycle = true;
//确保LayoutState 不为空,LayoutState 是保存
ensureLayoutState();
final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
final int absDy = Math.abs(dy);
updateLayoutState(layoutDirection, absDy, true, state);
//重点是在fill 函数,
final int consumed = mLayoutState.mScrollingOffset
+ fill(recycler, mLayoutState, state, false);
if (consumed < 0)
if (DEBUG)
Log.d(TAG, "Don't have any more elements to scroll");
return 0;
final int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
mOrientationHelper.offsetChildren(-scrolled);
mLayoutState.mLastScrollDelta = scrolled;
return scrolled;
关于fill 函数,在整体流程、Measure、Layout 详解——RecyclerView源码详解(一) 疑问已经分析过了,它的返回值,表示的是填充的值(即使没有显示在屏幕上),在有回收ViewHolder的功能 recycleByLayoutState(recycler, layoutState);
在LinearLayoutManager.java中
private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState)
if (!layoutState.以上是关于缓存——RecyclerView源码详解的主要内容,如果未能解决你的问题,请参考以下文章