如今在开发中RecyclerView已经基本替代了ListView来实现列表展示。而RecyclerView被说的最多的就是它的缓存机制,这也理所当然成了面试官的最爱啦。所以弄明白它的缓存机制就成了我们这些开发人员的必修课啦!
而要弄明白它的缓存机制,我们就要一点点的去抠它的源码,有人问这一步能不能省了。我想说我也想省,但是现实告诉我们,只看几篇博客(包括我这篇)而不去抠几遍源码时无法理解透彻RecyclerView的缓存机制的,到头来我们可能只知道有四级缓存:
缓存结构 |
说明 |
|
一级缓存 |
mChangedScrap 与 mAttachedScrap |
onLayout过程中屏幕内或正在移出屏幕的Item |
二级缓存 |
mCachedViews |
移出屏幕的Item,默认大小2 |
三级缓存 |
mViewCacheExtension |
ViewCacheExtension Google留给开发者自定义的缓存 |
四级缓存 |
mRecyclerPool |
当mCachedViews缓存满后会根据FIFO的规则将二级缓存中移出的ViewHolder缓存到RecycledViewPool中,默认大小5 |
但是却不明白ViewHolder在这四级缓存中是如何回收复用的。所以今天我们要做的就是通过源码弄明白这些不容易理解的知识点。
我们都知道只有在滑动的时候才会出现ViewHolder的回收复用现象。而滑动跟着的必然是Layout。而实际上RecyclerView的回收机制就是分onLayout和onTouchEvent这两条线的。但是由于篇幅和可读性关系,这里我只会对关键部分的源码进行分析。具体的流程可跟着流程图抠一遍源码。
public boolean onTouchEvent(MotionEvent e) {
....
1、一:首先将事件下发到子Item,这里是针对item有设置setOnTouchListener的事件下发情况。
if (dispatchOnItemTouch(e)) {
cancelTouch();
return true;
}
....
2、二:判断LayoutManager是否允许RecyclerView布局上下或者左右滑动。
如果我们有需要禁用RecyclerView某个方向滑动的需求就可以通过重写LayoutManager这两个方法的方式来实现
final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
final boolean canScrollVertically = mLayout.canScrollVertically();
.......
switch (action) {
.......
3、三:这里才是我们需要关注的重点,RecyclerView的回收复用就出现在它滑动的时候
case MotionEvent.ACTION_MOVE: {
final int index = e.findPointerIndex(mScrollPointerId);
if (index < 0) {
Log.e(TAG, "Error processing scroll; pointer index for id "
+ mScrollPointerId + " not found. Did any MotionEvents get skipped?");
return false;
}
........
if (mScrollState == SCROLL_STATE_DRAGGING) {
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
//但是看了半天发现具体的逻辑被分装在了scrollByInternal方法中
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;
......
}
return true;
}
void scrollStep(int dx, int dy, @Nullable int[] consumed) {
.......
/**我们都知道,RecyclerView是可以通过LayoutManager来设置横向还是竖向滑动的,
*只是方向处理上有差距,和我们这里要分析的并没有多大关联,所以我们只需要分析一个方向就好
*了。
*/
if (dx != 0) {
consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
}
if (dy != 0) {
consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
}
........
}
int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
....
//很多人可能会和我一样,看到这里就懵了,感觉路断了,因为跟到现在还是没有看到回收或者复用ViewHolder的逻辑,
难道是updateLayoutState方法?但是一看还是不对。
于是只能硬着头皮继续往下看了,找了半天找到了fill方法。这才是真正的入口啊!
所以看源码真的好难呀,一不小心就迷路了,到最后都不知道自己在干嘛,要干嘛,有同感的小伙伴请点赞。
updateLayoutState(layoutDirection, absDy, true, state);
final int consumed = mLayoutState.mScrollingOffset
+ fill(recycler, mLayoutState, state, false);
....
return scrolled;
}
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;
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
// TODO ugly bug fix. should not happen
//这是Google工程师的注释,看人家多谦虚,直接吐槽自己写的bug太ugly了。
//所以我们要做的就是连人家ugly的代码都要研究透彻
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
//重点一:回收机制
recycleByLayoutState(recycler, layoutState);
}
int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
if (RecyclerView.VERBOSE_TRACING) {
TraceCompat.beginSection("LLM LayoutChunk");
}
//重点二:复用机制
layoutChunk(recycler, state, layoutState, layoutChunkResult);
.....
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
......
}
.......
return start - layoutState.mAvailable;
}
讲到这里就出现了两个关键点了,一个回收,一个复用,这里我们就先分析回收机制
private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
if (!layoutState.mRecycle || layoutState.mInfinite) {
return;
}
//这里就出现了两个方向了,这里我们只分析一个方向,因为原理都是一样的。
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
//向下滑动,底部回收
recycleViewsFromEnd(recycler, layoutState.mScrollingOffset);
} else {
//向上滑动,顶部回收
recycleViewsFromStart(recycler, layoutState.mScrollingOffset);
}
}
//不管是哪个方向上的回收,最终调用的都是recycleChildren
private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) {
if (startIndex == endIndex) {
return;
}
//这里循环执行判断哪些item需要回收
if (endIndex > startIndex) {
for (int i = endIndex - 1; i >= startIndex; i--) {
removeAndRecycleViewAt(i, recycler);
}
} else {
for (int i = startIndex; i > endIndex; i--) {
removeAndRecycleViewAt(i, recycler);
}
}
}
public void recycleView(@NonNull View view) {
// This public recycle method tries to make view recycle-able since layout manager
// intended to recycle this view (e.g. even if it is in scrap or change cache)
ViewHolder holder = getChildViewHolderInt(view);//找到与View绑定的ViewHolder对象
if (holder.isTmpDetached()) {//如果已经是detach状态,则先从视图上移出。
removeDetachedView(view, false);
}
if (holder.isScrap()) {//判断是否已经在mChangedScrap或者mAttachedScrap中
holder.unScrap();//从mAttachedScrap或者mChangedScrap中移除
} else if (holder.wasReturnedFromScrap()) {
holder.clearReturnedFromScrapFlag();
}
recycleViewHolderInternal(holder);//执行真正的回收逻辑,将ViewHolder缓存到mCachedViews和Pool中
}
这里要说一点就是,RecyclerView的回收复用机制的逻辑都是由Recycler 这个内部类来完成的,这也体现了Google工程师在开发时遵循的单一原则等设计原则,让逻辑更加清晰。我们先去看一眼Recycler 这个类。
public final class Recycler {
1、一级缓存,回收布局过程中,屏幕可视范围内的VH
final ArrayList mAttachedScrap = new ArrayList<>();
ArrayList mChangedScrap = null;
2、二级缓存,回收已移出屏幕的VH
final ArrayList mCachedViews = new ArrayList();
private final List
mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);
private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
int mViewCacheMax = DEFAULT_CACHE_SIZE;
4、四级缓存,回收池
RecycledViewPool mRecyclerPool;
3、三级缓存,虽然通常我们不会用到这一级缓存
private ViewCacheExtension mViewCacheExtension;
static final int DEFAULT_CACHE_SIZE = 2;//mCachedViews 的默认大小
}
再看一眼RecycledViewPool的源码
public static class RecycledViewPool {
private static final int DEFAULT_MAX_SCRAP = 5;//每种viewType对应的默认大小
static class ScrapData {
final ArrayList mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
long mCreateRunningAverageNs = 0;
long mBindRunningAverageNs = 0;
}
//可以看到RecycledViewPool内部是个SparseArray的结构,
//即Key、Value的结构。而这里的Key就对应着viewType,value对应着ViewHolder
SparseArray mScrap = new SparseArray<>();
}
再来看一下RecycledViewPool的结构图:
void recycleViewHolderInternal(ViewHolder holder) {
.....
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
int cachedViewSize = mCachedViews.size();
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
//mViewCacheMax为mCachedViews缓存区的上限大小,默认为2。
当缓存区满,则移出最老的缓存视图,被移出的视图将被缓存至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.add(targetCacheIndex, holder);
cached = true;
}
if (!cached) {//如果未被缓存到mCachedViews中,则缓存至RecycledViewPool中
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
}
// 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) {//如果ViewHolder未被缓存,则将被永久丢弃。
holder.mOwnerRecyclerView = null;
}
}
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
......
detachAndScrapAttachedViews(recycler);//填充之前先回收所有Item
mLayoutState.mInfinite = resolveIsInfinite();
mLayoutState.mIsPreLayout = state.isPreLayout();
//这里还是分两个方向
if (mAnchorInfo.mLayoutFromEnd) {//向上滑动布局
// fill towards start
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtra = extraForStart;
fill(recycler, mLayoutState, state, false);//多次执行回收复用逻辑
startOffset = mLayoutState.mOffset;
final int firstElement = mLayoutState.mCurrentPosition;
if (mLayoutState.mAvailable > 0) {
extraForEnd += mLayoutState.mAvailable;
}
// fill towards end
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtra = extraForEnd;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state, false);
} else {//向下滑动布局
// fill towards end
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtra = extraForEnd;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
final int lastElement = mLayoutState.mCurrentPosition;
if (mLayoutState.mAvailable > 0) {
extraForStart += mLayoutState.mAvailable;
}
// fill towards start
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtra = extraForStart;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
if (mLayoutState.mAvailable > 0) {
extraForEnd = mLayoutState.mAvailable;
// start could not consume all it should. add more items towards end
updateLayoutStateToFillEnd(lastElement, endOffset);
mLayoutState.mExtra = extraForEnd;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
}
}
....
}
public void detachAndScrapAttachedViews(Recycler recycler) {
final int childCount = getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
final View v = getChildAt(i);
scrapOrRecycleView(recycler, i, v);
}
}
/**
* 1.Recycle操作对应的是removeView, View被remove后调用Recycler的recycleViewHolderInternal回收其ViewHolder
2.Scrap操作对应的是detachView,View被detach后调用Reccyler的scrapView暂存其ViewHolder
* @param recycler
* @param index
* @param view
*/
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
if (viewHolder.shouldIgnore()) {
if (DEBUG) {
Log.d(TAG, "ignoring view " + viewHolder);
}
return;
}
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {
//remove : VH无效且没有移除,且没有StableIds
removeViewAt(index);
//往cacheview和pool中添加ViewHolder
recycler.recycleViewHolderInternal(viewHolder);
} else {
//detach : 也就是还在屏幕可视范围内
detachViewAt(index);
//存到scrap中:mChangedScrap 与 mAttachedScrap
recycler.scrapView(view);
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
}
}
这里就可以看到分为Remove和Detach两种,所以我们需要区分两个概念,Detach和Remove
Detach: 首先我们应该了解每个ViewGroup都会经历两次onLayout过程,而RecyclerView在这个过程中会将屏幕范围内的childView临时从ChildView数组中remove, 但并不是真的移除,childView此时和View树还是藕断丝连的。View被detach一般是临时的,在后面会被重新attach.而这些被临时remove的View将被缓存到mAttachedScrap 与 mChangedScrap中。缓存时机是在onLayout的过程中,并且用完即清空。
remove: 真正的移除(真正移出屏幕可视范围内),不光被从ChildView数组中除名,其他和View树各项联系也会被彻底斩断, 比如焦点被清除,从TouchTarget中被移除等。此时会执行recycleViewHolderInternal(viewHolder)方法,而这个方法最终会将ViewHolder加入CacheView和Pool中。注意这里缓存的都是ViewHolder。
Detach :将屏幕范围内的VH回收到mAttachedScrap 与 mChangedScrap中
void scrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
|| !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
//非移除和无效VH、非Update VH:即在屏幕范围内的VH
holder.setScrapContainer(this, false);
mAttachedScrap.add(holder);
} else {
//移除、无效VH:即真正移出屏幕范围内的VH
if (mChangedScrap == null) {
mChangedScrap = new ArrayList();
}
holder.setScrapContainer(this, true);
mChangedScrap.add(holder);
}
}
Remove:回收移除屏幕的VH,逻辑与上面一致,就不再重写一遍了。