【Android】RecyclerView回收复用机制
概述
RecyclerView
是 Android 中用于高效显示大量数据的视图组件,它是 ListView 的升级版本,支持更灵活的布局和功能。
我们创建一个RecyclerView的Adapter:
public class MyRecyclerView extends RecyclerView.Adapter<MyRecyclerView.MyHolder> {
private List<String> strings;
private Context context;
public MyRecyclerView(List<String> strings, Context context) {
this.strings = strings;
this.context = context;
}
@NonNull
@Override
public MyRecyclerView.MyHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(android.R.layout.simple_list_item_1, parent, false);
MyRecyclerView.MyHolder viewHolder = new MyHolder(view);
Log.d("MyRecyclerView", "onCreateViewHolder: ");
return viewHolder;
}
@Override
public void onBindViewHolder(@NonNull MyRecyclerView.MyHolder holder, int position) {
holder.textView.setText("第" + position + "项");
Log.d("MyRecyclerView", "onBindViewHolder: " + position);
}
@Override
public int getItemCount() {
return strings == null ? 0 : strings.size();
}
public class MyHolder extends RecyclerView.ViewHolder {
private TextView textView;
public MyHolder(@NonNull View itemView) {
super(itemView);
textView = itemView.findViewById(android.R.id.text1);
}
}
}
我们在onCreateViewHolder和onBindViewHolder都打印log。
onCreateViewHolder()
会在创建一个新view的时候调用,onBindViewHolder()
会在已存在view,绑定数据的时候调用。
我们来看一下运行时打印的log:
在最开始加载view的时候,两个方法onCreateViewHolder()
和onBindViewHolder()
都执行了,但是当我们上下滑动RecyclerView的时候,我们会发现只执行了onBindViewHolder()
方法。所以说,RecyclerView并不是会一直重新创建View,而是会对view进行复用。
复用机制
当我们想去通过看源码去了解缓存复用机制的时候,我们要去想看源码的入口在哪里。上文我们提到是在滑动RecyclerView的时候进行了缓存复用,所以我们会想到去看 onTouchEvent
这个方法:
@Override
public boolean onTouchEvent(MotionEvent e) {
...
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;
}
final int x = (int) (e.getX(index) + 0.5f);
final int y = (int) (e.getY(index) + 0.5f);
int dx = mLastTouchX - x;
int dy = mLastTouchY - y;
if (mScrollState != SCROLL_STATE_DRAGGING) {
boolean startScroll = false;
if (canScrollHorizontally) {
if (dx > 0) {
dx = Math.max(0, dx - mTouchSlop);
} else {
dx = Math.min(0, dx + mTouchSlop);
}
if (dx != 0) {
startScroll = true;
}
}
if (canScrollVertically) {
if (dy > 0) {
dy = Math.max(0, dy - mTouchSlop);
} else {
dy = Math.min(0, dy + mTouchSlop);
}
if (dy != 0) {
startScroll = true;
}
}
if (startScroll) {
setScrollState(SCROLL_STATE_DRAGGING);
}
}
if (mScrollState == SCROLL_STATE_DRAGGING) {
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
if (dispatchNestedPreScroll(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
mReusableIntPair, mScrollOffset, TYPE_TOUCH
)) {
dx -= mReusableIntPair[0];
dy -= mReusableIntPair[1];
// Updated the nested offsets
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
// Scroll has initiated, prevent parents from intercepting
getParent().requestDisallowInterceptTouchEvent(true);
}
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
e)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
if (mGapWorker != null && (dx != 0 || dy != 0)) {
mGapWorker.postFromTraversal(this, dx, dy);
}
}
} break;
...
return true;
}
在case:MotionEvent.ACTION_MOVE里有 scrollByInternal()
这个方法:
boolean scrollByInternal(int x, int y, MotionEvent ev) {
int unconsumedX = 0;
int unconsumedY = 0;
int consumedX = 0;
int consumedY = 0;
consumePendingUpdateOperations();
if (mAdapter != null) {
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
scrollStep(x, y, mReusableIntPair);
consumedX = mReusableIntPair[0];
consumedY = mReusableIntPair[1];
unconsumedX = x - consumedX;
unconsumedY = y - consumedY;
}
if (!mItemDecorations.isEmpty()) {
invalidate();
}
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
TYPE_TOUCH, mReusableIntPair);
unconsumedX -= mReusableIntPair[0];
unconsumedY -= mReusableIntPair[1];
boolean consumedNestedScroll = mReusableIntPair[0] != 0 || mReusableIntPair[1] != 0;
// Update the last touch co-ords, taking any scroll offset into account
mLastTouchX -= mScrollOffset[0];
mLastTouchY -= mScrollOffset[1];
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
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 consumedNestedScroll || consumedX != 0 || consumedY != 0;
}
里面的 scrollStep()
方法:
void scrollStep(int dx, int dy, @Nullable int[] consumed) {
startInterceptRequestLayout();
onEnterLayoutOrScroll();
TraceCompat.beginSection(TRACE_SCROLL_TAG);
fillRemainingScrollValues(mState);
int consumedX = 0;
int consumedY = 0;
if (dx != 0) {
consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
}
if (dy != 0) {
consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
}
TraceCompat.endSection();
repositionShadowingViews();
onExitLayoutOrScroll();
stopInterceptRequestLayout(false);
if (consumed != null) {
consumed[0] = consumedX;
consumed[1] = consumedY;
}
}
scrollHorizontallyBy
与 scrollVerticallyBy
:
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
RecyclerView.State state) {
if (mOrientation == VERTICAL) {
return 0;
}
return scrollBy(dx, recycler, state);
}
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
RecyclerView.State state) {
if (mOrientation == HORIZONTAL) {
return 0;
}
return scrollBy(dy, recycler, state);
}
两个都执行的 scrollBy
:
int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getChildCount() == 0 || delta == 0) {
return 0;
}
ensureLayoutState();
mLayoutState.mRecycle = true;
final int layoutDirection = delta > 0 ? LinearLayoutManager.LayoutState.LAYOUT_END : LinearLayoutManager.LayoutState.LAYOUT_START;
final int absDelta = Math.abs(delta);
updateLayoutState(layoutDirection, absDelta, true, state);
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 = absDelta > consumed ? layoutDirection * consumed : delta;
mOrientationHelper.offsetChildren(-scrolled);
if (DEBUG) {
Log.d(TAG, "scroll req: " + delta + " scrolled: " + scrolled);
}
mLayoutState.mLastScrollDelta = scrolled;
return scrolled;
}
里面的 fill
方法最为关键:
int fill(RecyclerView.Recycler recycler, LinearLayoutManager.LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
...
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
if (RecyclerView.VERBOSE_TRACING) {
TraceCompat.beginSection("LLM LayoutChunk");
}
layoutChunk(recycler, state, layoutState, layoutChunkResult);
if (RecyclerView.VERBOSE_TRACING) {
TraceCompat.endSection();
}
if (layoutChunkResult.mFinished) {
break;
}
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
/**
* Consume the available space if:
* * layoutChunk did not request to be ignored
* * OR we are laying out scrap children
* * OR we are not doing pre-layout
*/
if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
|| !state.isPreLayout()) {
layoutState.mAvailable -= layoutChunkResult.mConsumed;
// we keep a separate remaining space because mAvailable is important for recycling
remainingSpace -= layoutChunkResult.mConsumed;
}
if (layoutState.mScrollingOffset != LinearLayoutManager.LayoutState.SCROLLING_OFFSET_NaN) {
layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
if (stopOnFocusable && layoutChunkResult.mFocusable) {
break;
}
}
if (DEBUG) {
validateChildOrder();
}
return start - layoutState.mAvailable;
}
这个方法功能是填充给定的布局,通过while循环不断进行填充,其中的 layoutChunk()
方法:
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
View view = layoutState.next(recycler);//获取下一项需要布局的视图
if (view == null) {
if (DEBUG && layoutState.mScrapList == null) {
throw new RuntimeException("received null view when unexpected");
}
// if we are laying out views in scrap, this may return null which means there is
// no more items to layout.
result.mFinished = true;
return;
}
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
if (layoutState.mScrapList == null) {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addView(view);//将视图添加到布局的末尾
} else {
addView(view, 0);//将视图添加到布局的开头
}
} else {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addDisappearingView(view);
} else {
addDisappearingView(view, 0);
}
}
...
}
依次点击:
最后我们就找到了回收复用的最关键的代码。
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
if (position < 0 || position >= mState.getItemCount()) {
throw new IndexOutOfBoundsException("Invalid item position " + position
+ "(" + position + "). Item count:" + mState.getItemCount()
+ exceptionLabel());
}
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) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if (holder != null) {
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;
}
}
}
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()) {
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.
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
if (holder == null) {
throw new IllegalArgumentException("getViewForPositionAndType returned"
+ " a view which does not have a ViewHolder"
+ exceptionLabel());
} else if (holder.shouldIgnore()) {
throw new IllegalArgumentException("getViewForPositionAndType returned"
+ " a view that is ignored. You must call stopIgnoring before"
+ " returning this view." + exceptionLabel());
}
}
}
if (holder == null) { // fallback to pool
if (DEBUG) {
Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
+ position + ") fetching from shared pool");
}
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;
}
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);
if (DEBUG) {
Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder");
}
}
}
// This is very ugly but the only place we can grab this information
// before the View is rebound and returned to the LayoutManager for post layout ops.
// We don't need this in pre-layout since the VH is not updated by the LM.
if (fromScrapOrHiddenOrCache && !mState.isPreLayout() && holder
.hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) {
holder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
if (mState.mRunSimpleAnimations) {
int changeFlags = ItemAnimator
.buildAdapterChangeFlagsForAnimations(holder);
changeFlags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
final ItemHolderInfo info = mItemAnimator.recordPreLayoutInformation(mState,
holder, changeFlags, holder.getUnmodifiedPayloads());
recordAnimationInfoIfBouncedHiddenView(holder, info);
}
}
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()) {
if (DEBUG && holder.isRemoved()) {
throw new IllegalStateException("Removed holder should be bound and it should"
+ " come here only in pre-layout. Holder: " + holder
+ exceptionLabel());
}
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
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;
}
从代码中我们可以看出,复用的并不是一个个控件,而是 ViewHolder(ItemView)
。
我们可以通过上面代码看出来RecyclerView的复用机制:
第一层:Changed Scrap
代码:
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
解释:
- 如果当前处于预布局(
isPreLayout == true
),会优先从变更缓存中获取。 getChangedScrapViewForPosition(position)
查找变更缓存(mChangedScrap
),这里保存的是那些被标记为需要更新的ViewHolder
。- 如果找到,直接返回,不需要从其他层中查找。
在
RecyclerView
的布局过程中,预布局(Pre-Layout) 是RecyclerView
为支持动画效果(如插入、删除、移动等操作)而执行的一个特殊布局阶段。以下操作都会触发 预布局阶段,并可能从变更缓存中获取视图来进行后续处理:
- 插入、移除、范围更新(
notifyItemInserted
,notifyItemRemoved
,notifyItemRangeChanged
等)- 视图的布局和数据绑定(例如,
setAdapter
,setHasStableIds
)- 布局管理器或动画的变化(
setLayoutManager
,setItemAnimator
)- 视图的移动(
notifyItemMoved
)- 所有与动画相关的操作(包括添加、删除、移动动画)
第二层:Scrap/Hidden/Cache
代码:
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if (holder != null) {
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;
}
}
}
解释:
-
如果第一层缓存未命中,尝试从
普通缓存层中获取,包括:
- Scrap:表示那些视图项暂时不可见但还可以复用的视图,它们是最常见的一种缓存方式。被标记为废弃的视图,在没有被回收之前可以复用。
- Hidden:隐藏视图与
Scrap
类似,但它们的存在通常是为了支持更复杂的布局切换、动画等。它们在显示区域外,但仍然保留在缓存中,直到需要重新显示。 - Cache:
RecyclerView
使用缓存来存储那些根据视图 ID 或类型等条件频繁访问的视图。它们通常在视图池中存储较长时间,直到达到缓存容量限制。
-
调用
validateViewHolderForOffsetPosition(holder)
检查缓存的有效性:
- 如果无效(比如位置错位),将其回收。
- 如果有效,标记
fromScrapOrHiddenOrCache = true
。
第三层:Stable ID Cache
代码:
if (holder == null && mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
if (holder != null) {
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
}
}
解释:
- 如果Adapter支持稳定ID(
hasStableIds == true
),尝试通过稳定ID查找缓存中的ViewHolder
。 - 调用
getScrapOrCachedViewForId
,通过ID获取匹配的ViewHolder
。 - 如果找到,将其位置更新为
offsetPosition
,并标记为来自缓存。
如何启用 Stable ID?
为了使
RecyclerView
使用 Stable ID Cache,必须确保以下两点:
- 实现
hasStableIds()
方法: 你需要在你的RecyclerView.Adapter
中重写hasStableIds()
方法并返回true
。这是启用 Stable ID Cache 的前提。- 返回稳定的 ID: 在适配器的
getItemId()
方法中为每个项返回唯一的 ID。这个 ID 通常是数据中的唯一标识符(比如数据库中的主键)。示例代码:
public class MyAdapter extends RecyclerView.Adapter<MyViewHolder> { @Override public boolean hasStableIds() { return true; // 启用 Stable ID } @Override public long getItemId(int position) { // 假设你的数据项有一个唯一的 id 字段 return myDataList.get(position).getId(); } // 其他适配器方法... }
或者在构造方法中进行设置:
public MyRecyclerViewAdapter(List<String> strings, Context context) { this.strings = strings; this.context = context; setHasStableIds(true); // 启用稳定 ID }
第四层:Recycled Pool
代码:
if (holder == null) {
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
}
}
解释:
-
Recycled Pool
是RecyclerView
内部维护的一个缓存池,用于存储已经被回收并不再使用的视图(View)。这些视图通常是已经滑出屏幕或者暂时不可见的视图。通过回收池,RecyclerView
可以避免每次滚动时都重新创建视图,而是将已回收的视图重新利用,从而提升滚动性能。回收池的工作机制是,
RecyclerView
会在视图不再需要时将它们放入回收池(即已回收的视图池)。当需要新的视图时,RecyclerView
会从回收池中获取一个合适的视图进行重用。 -
回收池存储的是所有超出缓存数量限制的
ViewHolder
,按type
分类。 -
如果找到,调用
resetInternal()
重置其状态。
最后一层:创建新 ViewHolder
代码中涉及的部分:
if (holder == null) {
holder = mAdapter.createViewHolder(RecyclerView.this, type);
}
说明:
- 如果所有缓存机制都未找到匹配的
ViewHolder
,最终会调用Adapter.createViewHolder
来创建新的实例。 - 这是性能代价最高的一步。
我们通过点击
就可以找到我们每次写Adapter都用重写的 onCreateViewHolder
方法了。
在后面的代码中,我们依次点击:
就可以找到我们每次写Adapter都用重写的 onBindViewHolder
方法了。
已经到底啦!!