三个场景带你了解RecyclerView
说明: RecyclerView的版本是23.2.1,RecyclerView的布局为match_parent
,使用到的LayoutManager为LinearLayoutManager。如果你真想搞懂这几个场景的代码,建议自己写个demo,然后断点调试,再结合这篇文章,效果会更好一点。
场景一 RecyclerView绘制流程
RecyclerView虽然很复杂,可它实质上也是一个View,遵循View的绘制流程。所以想要了解RecyclerView,可以从它的绘制流程下手。首先从onMeasure
开始。(PS:这里只分析match_parent
的情况。)
protected void onMeasure(int widthSpec, int heightSpec) {
if (mLayout == null) {
defaultOnMeasure(widthSpec, heightSpec);
return;
}
// 从23.2.0之后版本的RecyclerView都已经支持自动测量了。
// 可通过LayoutManager.setAutoMeasureEnabled(true)设置自动测量。
// 在LinearLayoutManager中默认开启自动测量。
if (mLayout.mAutoMeasure) {
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
// 如果RecyclerView的宽和高都指定为match_parent或者具体的值,skipMeasure为true。
// 在我们分析的这种场景中,skipMeasure为true。
final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
&& heightMode == MeasureSpec.EXACTLY;
// 调用LayoutManager的测量。
// 默认实现为RecyclerView.defaultOnMeasure
// LinearLayoutManager使用的是默认的测量方式
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
// 完成测量,直接返回。
if (skipMeasure || mAdapter == null) {
return;
}
// 如果RecyclerView的宽和高任意一个指定为wrap_content,还需要进行布局以确定大小。
// 省略了很多代码
......
} else {
// 非自动测量的情况
......
}
}
RecyclerView的默认测量方式:
void defaultOnMeasure(int widthSpec, int heightSpec) {
// calling LayoutManager here is not pretty but that API is already public and it is better
// than creating another method since this is internal.
final int width = LayoutManager.chooseSize(widthSpec,
getPaddingLeft() + getPaddingRight(),
ViewCompat.getMinimumWidth(this));
final int height = LayoutManager.chooseSize(heightSpec,
getPaddingTop() + getPaddingBottom(),
ViewCompat.getMinimumHeight(this));
setMeasuredDimension(width, height);
}
从onMeasure
的代码中可以看出,RecyclerView的宽和高最好能指定为match_parent
或者具体值,这样可以省下不少测量的工作。
测量完RecyclerView之后,就到布局了。onLayout
内部会调用dispatchLayout
方法。dispatchLayoutStep
分为三个步骤:
- dispatchLayoutStep1: Adapter的更新; 决定该启动哪种动画; 保存当前View的信息; 如果有必要,先跑一次布局并将信息保存下来。
- dispatchLayoutStep2: 真正对子View做布局的地方。
- dispatchLayoutStep3: 为动画保存View的相关信息; 触发动画; 相应的清理工作。
void dispatchLayout() {
...... // 省略代码
mState.mIsMeasuring = false;
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth() ||
mLayout.getHeight() != getHeight()) {
// First 2 steps are done in onMeasure but looks like we have to run again due to
// changed size.
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else {
// always make sure we sync them (to ensure mode is exact)
mLayout.setExactMeasureSpecsFrom(this);
}
dispatchLayoutStep3();
}
在dispatchLayoutStep1
中会触发processAdapterUpdatesAndSetAnimationFlags
方法,如它的名字一样,它的作用是通过AdapterHelper来更新Adapter; 记录动画的标志,决定该启动哪种类型的动画。
说dispatchLayoutStep2
是真正的布局,是因为它调用了mLayout.onLayoutChildren
,而这也是不同的LayoutManager布局不同的根本原因。我们以LinearLayoutManager为例,它里面有两个比较重要的方法:detachAndScrapAttachedViews
和fill
。detachAndScrapAttachedViews
会回收当前所有屏幕上的子View到Scarp中,虽然该方法很重要,但对于RecyclerViewr的第一次初始化,没有什么好回收的,所以直接忽略。来看看fill
方法。
// fill方法的任务是向RecyclerView中填充子View,终止条件为:
// 1). 没有空余的空间了
// 2). stopOnFocusable为true并且遇到了第一个focusable的子View
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
final int start = layoutState.mAvailable;
// 表示有滑动,先进行一次回收。
// 初始化时不会走这里面。
if (layoutState.mScrollingOffset != LayoutState.SCOLLING_OFFSET_NaN) {
// TODO ugly bug fix. should not happen
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
// 回收子View。
recycleByLayoutState(recycler, layoutState);
}
int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
// 保存着对子View layout之后的结果
LayoutChunkResult layoutChunkResult = new LayoutChunkResult();
// 不断添加子View,直到结束
// 一般情况下,layoutState.mInfinite为false
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
// 重点在该方法里面
layoutChunk(recycler, state, layoutState, layoutChunkResult);
// 填充结束
if (layoutChunkResult.mFinished) {
break;
}
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
if (!layoutChunkResult.mIgnoreConsumed || mLayoutState.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 != LayoutState.SCOLLING_OFFSET_NaN) {
layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
// 回收子View。
recycleByLayoutState(recycler, layoutState);
}
// 如果新添加的子View是focusable的且设置了stopOnFocusable为true,停止填充子view
if (stopOnFocusable && layoutChunkResult.mFocusable) {
break;
}
}
...... // 省略代码
return start - layoutState.mAvailable;
}
fill
方法的重点是layoutChunk
方法,该方法大致流程如下:创建子View -> 将子View添加到RecyclerView中 -> 测量子View -> 对子View进行布局操作。
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
// 创建子View的地方
View view = layoutState.next(recycler);
...... // 省略代码
LayoutParams params = (LayoutParams) view.getLayoutParams();
// 将子view添加到RecyclerView中
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);
}
}
// 测量子View
measureChildWithMargins(view, 0, 0);
// 计算子view占用了多少空间,以便fill方法中计算剩余的空间
result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
...... // 省略代码
// We calculate everything with View's bounding box (which includes decor and margins)
// To calculate correct layout position, we subtract margins.
// 对子View进行layout过程
layoutDecorated(view, left + params.leftMargin, top + params.topMargin,
right - params.rightMargin, bottom - params.bottomMargin);
...... // 省略代码
result.mFocusable = view.isFocusable();
}
创建View:
子View通过layoutState.next(recycler)
被创建出来,next
内部最后又会调用到了Recycler.getViewForPosition
,Recycler是整个RecyclerView实现回收复用的关键。它会尝试从多个地方获取已缓存起来的ViewHolder,如果最终获取失败,才会去创建ViewHolder,对于第一次初始化来说,最终都会去创建ViewHolder,即回调我们所熟悉的onCreateViewHolder
方法。以下的代码只截取了关键部分。
View getViewForPosition(int position, boolean dryRun) {
boolean fromScrap = false;
ViewHolder holder = null;
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
final int type = mAdapter.getItemViewType(offsetPosition);
if (holder == null) {
// 创建ViewHolder,方法内部会回调我们熟悉的onCreateViewHolder
holder = mAdapter.createViewHolder(RecyclerView.this, type);
}
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()) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
// 先设置OwnerRecyclerView,然后绑定ViewHolder
holder.mOwnerRecyclerView = RecyclerView.this;
// 内部会回调我们熟悉的onBindViewHolder,并且会设置flag,holder.isBound()将会返回true。
mAdapter.bindViewHolder(holder, offsetPosition);
}
// 设置LayoutParams
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 = fromScrap && bound;
return holder.itemView;
}
添加View:
添加,删除等这些与ViewGroup相关的操作都是放在ChildHelper中去进行的,原因是这里除了通知RecyclerView做相应的操作之外,还可能做了其它的操作,比如回调,设置标志位等。
@Override
public void addView(View child, int index) {
// 添加子view
RecyclerView.this.addView(child, index);
// 分发事件,回调Adapter.onViewAttachedToWindow方法。
dispatchChildAttached(child);
}
总结一下,从fill
开始,一个View从创建到添加到布局经历了这些流程:
LinearLayoutManager.layoutChunk() -> Recycler.getViewForPosition() -> Adapter.onCreateViewHolder() -> 设置ownerRecyclerView -> Adapter.onBindViewHolder() -> 设置LayoutParams -> RecyclerView.addView() -> Adapter.onViewAttachedToWindow() -> LayoutManager.measureChildWithMargins() -> LayoutManager.layoutDecorated()
场景二 RecyclerView的滚动与Recycler的回收
先看三张截图,第一张是RecyclerView刚初始化完,屏幕上总共有6个子View;第二张是开始滑动了,可以看出,6, 7, 8 都是通过重新创建ViewHolder生成的,在这期间,0, 1, 2 都已经从RecyclerView上移除了。然后从第9开始,RecyclerView开始复用了。但是只有第0和2被回收掉了,这中间漏了1。如果再继续滑动的时候,变成1和4被回收了。看起来好像没什么规律!所以这一小节,通过RecyclerView的滑动来分析它的回收机制。



当用户触发滑动的时候,RecyclerView会先进行一次View的回收,然后往RecyclerView中填充子View,然后又再进行了一次回收。子View的回收有可能是在填充新的子View之前,也可能是之后,所以进行了两次回收工作。
以LinearLayoutManager为例, 当滚动的时候,方法调用如下:onTouchEvent
-> scrollByInternal
-> LinearLayoutManager.scrollVerticallyBy
-> LinearLayoutManager.scrollBy
-> LinearLayoutManager.fill
.
再来看一次fill
的代码:
// fill方法的任务是向RecyclerView中填充子View,终止条件为:
// 1). 没有空余的空间了
// 2). stopOnFocusable为true并且遇到了第一个focusable的子View
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
final int start = layoutState.mAvailable;
// 表示有滑动,先进行一次回收。
if (layoutState.mScrollingOffset != LayoutState.SCOLLING_OFFSET_NaN) {
// TODO ugly bug fix. should not happen
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
// 回收子View。
recycleByLayoutState(recycler, layoutState);
}
int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
// 保存着对子View layout之后的结果
LayoutChunkResult layoutChunkResult = new LayoutChunkResult();
// 不断添加子View,直到结束
// 一般情况下,layoutState.mInfinite为false
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
// 重点在该方法里面
layoutChunk(recycler, state, layoutState, layoutChunkResult);
// 填充结束
if (layoutChunkResult.mFinished) {
break;
}
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
if (!layoutChunkResult.mIgnoreConsumed || mLayoutState.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 != LayoutState.SCOLLING_OFFSET_NaN) {
layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
// 回收子View。
recycleByLayoutState(recycler, layoutState);
}
// 如果新添加的子View是focusable的且设置了stopOnFocusable为true,停止填充子view
if (stopOnFocusable && layoutChunkResult.mFocusable) {
break;
}
}
......
return start - layoutState.mAvailable;
}
从fill
方法可以看出,回收子View的代码在recycleByLayoutState
方法中,但它其实也只是一个帮助的方法,用来判断从前面还是后面回收。调用顺序如下: (recycleViewsFromStart
/ recycleViewsFromEnd
) -> recycleChildren
-> removeAndRecycleViewAt
public void removeAndRecycleViewAt(int index, Recycler recycler) {
final View view = getChildAt(index);
// 调用ChildHelper.removeViewAt((),该方法做了两件事:
// 1) 通知Adapter和OnChildAttachStateChangeListener,有onViewDetachedFromWindow事件;
// 2) RecyclerView将相应的子View移除掉。
removeViewAt(index);
recycler.recycleView(view);
}
Recycler.recycleView
方法:
/**
* 回收一个已分离的子View,它会被加到缓冲池中以便后续的重绑和复用。
*
* 在回收之前,子View必须完全从RecyclerView中分离出来,如果该View是已废弃的(scrapped), 它会从scrap list中移除。
*
* @param view Removed view for recycling
* @see LayoutManager#removeAndRecycleView(View, Recycler)
*/
public void recycleView(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);
if (holder.isTmpDetached()) {
removeDetachedView(view, false);
}
// 如果该View是已废弃的(scrapped), 它会从Scrap列表中被移除。
if (holder.isScrap()) {
holder.unScrap();
} else if (holder.wasReturnedFromScrap()){
holder.clearReturnedFromScrapFlag();
}
recycleViewHolderInternal(holder);
}
Recycler.recycleViewHolderInternal
方法:
void recycleViewHolderInternal(ViewHolder holder) {
//noinspection unchecked
final boolean transientStatePreventsRecycling = holder
.doesTransientStatePreventRecycling();
final boolean forceRecycle = mAdapter != null
&& transientStatePreventsRecycling
&& mAdapter.onFailedToRecycleView(holder);
boolean cached = false;
boolean recycled = false;
if (forceRecycle || holder.isRecyclable()) {
if (!holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED
| ViewHolder.FLAG_UPDATE)) {
// Retire oldest cached view
final int cachedViewSize = mCachedViews.size();
// 如果mCachedView满了,从CachedView中取出第一个item,并放进RecycledViewPool
if (cachedViewSize == mViewCacheMax && cachedViewSize > 0) {
recycleCachedViewAt(0);
}
// 先往mCachedView队列加
if (cachedViewSize < mViewCacheMax) {
mCachedViews.add(holder);
cached = true;
}
}
// 如果还没缓存,存进RecycledViewPool
if (!cached) {
addViewHolderToRecycledViewPool(holder);
recycled = true;
}
} else if (DEBUG) {
Log.d(TAG, "trying to recycle a non-recycleable holder. Hopefully, it will "
+ "re-visit here. We are still removing it from animation lists");
}
// 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;
}
}
方法解释:
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
// 定义mCachedViews最大容量,默认为2.
private int mViewCacheMax = DEFAULT_CACHE_SIZE;
private static final int DEFAULT_CACHE_SIZE = 2;
recycleViewHolderInternal
方法做了两件事:
-
如果
mCachedView
没满,直接将ViewHolder存进mCachedViews中 -
如果
mCachedViews
满了,将第0个ViewHolder取出来放进RecycledViewPool;同时,新的ViewHolder也不会再放到mCachedViews中了。所以此时,mCachedViews
的容量为mViewCacheMax - 1
,从这也可以看出mCachedViews
其实就是在模拟队列(先进先出)。被添加到RecycledViewPool的ViewHolder会回调到Adapter.onViewRecycled
方法。
在上面的截图中,0和1先被放进mCachedView
中,当2进来的时候,由于mCachedView
已经满了,所以移除并回收0,同时回收2。所以是0和2被回收了,1还在mCachedView
队列中。
回收完之后有了空间,继续往RecyclerView中填充子View,与第一次布局(初始化)时候不一样的是,ViewHolder是从缓存中取出来的,而不是重新去创建出来的。看一下它的流程图:

从流程图可以看出RecyclerView至少有四级缓存,再用一张表格来总结一下:
缓存类型 | 创建ViewHolder | 绑定ViewHolder | 备注 |
---|---|---|---|
mAttachedScrap | 否 | 否 | 快速重建RecyclerView |
mCachedViews | 否 | 否 | 默认容量为2个 |
mViewCacheExtension | 需要开发者自己实现 | ||
mRecyclerPool | 否 | 是 | 多个RecyclerView可以共用一个 |
从滚动的过程来看,并没有涉及到mAttachedScrap
,只利用到了mCachedView
和RecycledViewPool,即getViewForPosition
方法中会先从mCachedView
去检查,没有的话再从RecycledViewPool去拿,并重新调用onBindViewHolder。到这里,应该也可以解释为什么打印出的Log会那么奇怪的问题了。
场景三 插入数据与RecyclerView的快速重绘
当我们调用notifyItemInserted
的时候,一般情况下,最终会触发requestLayout
,
-
dispatchLayoutStep1:RecyclerView将屏幕上的所有ViewHolder的位置做一个偏移处理(如果有需要的话),然后回收所有的ViewHolder至Scrap中。
-
dispatchLayoutStep2:从Scrap中拿出没有改变的ViewHolder,新加入的item从RecycledViewPool中获取或者走createViewHolder流程。
-
dispatchLayoutStep3:执行动画,在动画结束后回收不可见的View。
没带前缀的是RecyclerView的方法
Adapter.notifyItemInserted
-> AdapterDataObservable.notifyItemRangeInserted
-> RecyclerViewDataObserver.onItemRangeInserted
-> AdapterHelper.onItemRangeInserted
-> RecyclerViewDataObserver.triggerUpdateProcessor
-> requestLayout
-> onMeasure
-> onLayout
-> dispatchLayout
第一步:
dispatchLayoutStep1
-> processAdapterUpdatesAndSetAnimationFlags
-> AdapterHelper.preProcess
-> ... -> offsetPositionRecordsForInsert
-> Recycler.offsetPositionRecordsForInsert
偏移ViewHolder的位置,假设在第二个位置之前插入一个item,那么第0和第1个ViewHolder的位置都无需改变,第2个位置开始,位置都偏移itemCount
个位置。以第2个item为例:
mOldPosition = 2;
mPosition += itemCount;
偏移ViewHolder的位置之后,Recycler也需要更新mCachedViews
中ViewHolder的位置。
偏移ViewHolder的位置之后,会调用requestLayout
,走:dispatchLayoutStep1
-> LinearLayoutmanager.onLayoutChildren
-> detachAndScrapAttachedViews
。在detachAndScrapAttachedViews
中RecyclerView将所有的View分离出来,并放进mAttachedScrap
中。然后走fill
流程。
第二步:
dispatchLayoutStep2
,这里才是真正实现布局的地方。dispatchLayoutStep2
也会调用LinearLayoutmanager.onLayoutChildren
。这一次,ViewHolder从Scrap中取出,新加入的item从RecycledViewPool中获取或者走createViewHolder流程。
第三步:
dispatchLayoutStep3
,这个方法会触发相应的动画:item插入,其它item偏移,即将不可见的item做完动画后会被回收:ItemAnimatorRestoreListener.onAnimationFinished
-> removeAnimatingView
-> Recycler.unscrapView
-> Recycler.recycleViewHolderInternal
。
通过对ViewHolder做位置的偏移处理,并将所有的ViewHolder放到Scrap中,可以使得数据改变的时候,RecyclerView可以快速响应,并且这个过程中,如果插入一个item,那么最糟糕的情况是只需要再走一次创建ViewHolder的流程而已。
当有数据删除的时候情况与数据插入类似。