开篇:
要了解RecyclerView先从它的总体开始看起,防止管中窥豹可见一斑,一叶障目不见泰山。
RecyclerView家族谱
上图可以看出RecyclerView的设计是十分灵活的,它把自身的功能拆分成了好几个模块,使开发者可以根据自己的需要进行灵活的配置。
RecyclerView基本用法
我们先从RecyclerView的基本用法入手,开始了解RecyclerView。
// 在Activity的onCreate方法中,这样列表就能正常显示了
recycler_view.adapter = MyAdapter()
// 从此处开始追踪,分析点1
recycler_view.layoutManager = LinearLayoutManager(this,LinearLayoutManager.VERTICAL,false)
源码追踪
从上面我们可以看到RecyclerView的使用十分简单,仅仅根据几行代码就可以完成RecyclerView的配置和使用,那么让我们从setLayoutManager开始追根溯源,看看RecyclerView的奥秘所在。
// 分析点1:RecyclerView#setLayoutManager(@Nullable LayoutManager layout)方法
public void setLayoutManager(@Nullable LayoutManager layout) {
if (layout == mLayout) {
return;
}
stopScroll();
// TODO We should do this switch a dispatchLayout pass and animate children. There is a good
// chance that LayoutManagers will re-use views.
// 在setLayoutManager方法调用之前如果已经关联了LayoutManager,在此处就会进行旧LayoutManager的清除关联的操作
if (mLayout != null) {
// end all running animations
if (mItemAnimator != null) {
mItemAnimator.endAnimations();
}
mLayout.removeAndRecycleAllViews(mRecycler);
mLayout.removeAndRecycleScrapInt(mRecycler);
mRecycler.clear();
if (mIsAttached) {
mLayout.dispatchDetachedFromWindow(this, mRecycler);
}
mLayout.setRecyclerView(null);
mLayout = null;
} else {
mRecycler.clear();
}
// this is just a defensive measure for faulty item animators.
mChildHelper.removeAllViewsUnfiltered();
mLayout = layout;
// 判断新设定的LayoutManager是否已经和某RecyclerView进行了关联,如果有则抛出异常
if (layout != null) {
if (layout.mRecyclerView != null) {
throw new IllegalArgumentException("LayoutManager " + layout
+ " is already attached to a RecyclerView:"
+ layout.mRecyclerView.exceptionLabel());
}
mLayout.setRecyclerView(this);
if (mIsAttached) {
mLayout.dispatchAttachedToWindow(this);
}
}
mRecycler.updateViewCacheSize();
// 可以看到,最终setLayoutManager方法的最终还是调用了requestLayout方法
// 开启了ViewGroup的onMeasure和onLayout流程,分析点2
requestLayout();
}
接着只能接着分析RecyclerView的onMeasure和onLayout流程
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
// 此处defaultOnMeasure方法会尽可能给出一个宽高值,意义就是如果没设置LayoutManager则尽可能给RecyclerView一个兜底的宽高值
if (mLayout == null) {
defaultOnMeasure(widthSpec, heightSpec);
return;
}
// 如果设置的LayoutManager是开启了自动测量(一个标志位),如LinearLayoutManager就返回true
if (mLayout.isAutoMeasureEnabled()) {
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
/**
* This specific call should be considered deprecated and replaced with
* {@link #defaultOnMeasure(int, int)}. It can't actually be replaced as it could
* break existing third party code but all documentation directs developers to not
* override {@link LayoutManager#onMeasure(int, int)} when
* {@link LayoutManager#isAutoMeasureEnabled()} returns true.
*/
// 乍一眼看过去以为此处将子View的测量工作交给了LayoutManager去执行
// 其实追踪分析同样给RecyclerView一个兜底的宽高值,最终调用的还是defaultOnMeasure方法
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
// 这里表示如果宽高的测量模式都是确切值则不用进行测量了,可直接返回
final boolean measureSpecModeIsExactly =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
if (measureSpecModeIsExactly || mAdapter == null) {
return;
}
// mState这个实例保存的是RecyclerView滑动状态下的信息(ItemCount,layoutstep等)
// mState在RecyclerView被创建的同时被创建(初始化)
// 其中mLayoutStep有三个取值(STEP_START, STEP_LAYOUT, STEP_ANIMATIONS)是控制调用对应的dispatchLayoutStep123系列方法的
if (mState.mLayoutStep == State.STEP_START) {
/**
* The first step of a layout where we;
* - process adapter updates
* - decide which animation should run
* - save information about current views
* - If necessary, run predictive layout and save its information
*/
// 解释:在布局开始前收集需要动画的item,以及动画的信息
// 在该方法末尾会使mState.mLayoutStep = State.STEP_LAYOUT,表示信息收集完毕可以进行布局操作了
dispatchLayoutStep1();
}
// set dimensions in 2nd step. Pre-layout should happen with old dimensions for
// consistency
mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
// 测量布局的第二步,有可能调用两次
// 这个方法最后会使mState.mLayoutStep = State.STEP_ANIMATIONS,表示布局完成可以进行动画操作了,该方法下面会重点介绍
dispatchLayoutStep2();
// now we can get the width and height from the children.
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
// if RecyclerView has non-exact width and height and if there is at least one child
// which also has non-exact width & height, we have to re-measure.
// 如果RecyclerView没有确切的宽高,而且至少有一个子View没有确切宽高,则dispatchLayoutStep2就会调用两次来获得RecyclerView的宽高
if (mLayout.shouldMeasureTwice()) {
mLayout.setMeasureSpecs(
MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
mState.mIsMeasuring = true;
dispatchLayoutStep2();
// now we can get the width and height from the children.
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
}
} else {
if (mHasFixedSize) {
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
return;
}
// custom onMeasure
if (mAdapterUpdateDuringMeasure) {
startInterceptRequestLayout();
onEnterLayoutOrScroll();
processAdapterUpdatesAndSetAnimationFlags();
onExitLayoutOrScroll();
if (mState.mRunPredictiveAnimations) {
mState.mInPreLayout = true;
} else {
// consume remaining updates to provide a consistent state with the layout pass.
mAdapterHelper.consumeUpdatesInOnePass();
mState.mInPreLayout = false;
}
mAdapterUpdateDuringMeasure = false;
stopInterceptRequestLayout(false);
} else if (mState.mRunPredictiveAnimations) {
// If mAdapterUpdateDuringMeasure is false and mRunPredictiveAnimations is true:
// this means there is already an onMeasure() call performed to handle the pending
// adapter change, two onMeasure() calls can happen if RV is a child of LinearLayout
// with layout_width=MATCH_PARENT. RV cannot call LM.onMeasure() second time
// because getViewForPosition() will crash when LM uses a child to measure.
setMeasuredDimension(getMeasuredWidth(), getMeasuredHeight());
return;
}
if (mAdapter != null) {
mState.mItemCount = mAdapter.getItemCount();
} else {
mState.mItemCount = 0;
}
startInterceptRequestLayout();
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
stopInterceptRequestLayout(false);
mState.mInPreLayout = false; // clear
}
}
上面onMeasure的if分支追踪暂时告一段落,现在看下onLayout方法的调用
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
// onLayout这个方法基本没做什么,进而调用了dispatchLayout方法,进一步跟进
dispatchLayout();
TraceCompat.endSection();
mFirstLayoutComplete = true;
}
void dispatchLayout() {
if (mAdapter == null) {
Log.e(TAG, "No adapter attached; skipping layout");
// leave the state in START
return;
}
if (mLayout == null) {
Log.e(TAG, "No layout manager attached; skipping layout");
// leave the state in START
return;
}
mState.mIsMeasuring = false;
// 这个方法中同样是判断当时mState.mLayoutStep的值,确定Layout的状态,从而调用dispatchLayoutStep123方法
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
// 这里追踪dispatchLayoutStep2方法
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处理动画
dispatchLayoutStep3();
}
重点开始追踪dispatchLayoutStep2方法,也就是测量子View的开始
/**
* The second layout step where we do the actual layout of the views for the final state.
* This step might be run multiple times if necessary (e.g. measure).
*/
private void dispatchLayoutStep2() {
startInterceptRequestLayout();
onEnterLayoutOrScroll();
mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
mAdapterHelper.consumeUpdatesInOnePass();
mState.mItemCount = mAdapter.getItemCount();
mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;
// Step 2: Run layout
mState.mInPreLayout = false;
// 终于我们看到这里开始RecyclerView将测量和布局工作委托给LayoutManager
// 但是在LayoutManager中是空实现,如果自定义LayoutManager必须实现该方法
mLayout.onLayoutChildren(mRecycler, mState);
mState.mStructureChanged = false;
mPendingSavedState = null;
// onLayoutChildren may have caused client code to disable item animations; re-check
mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
mState.mLayoutStep = State.STEP_ANIMATIONS;
onExitLayoutOrScroll();
stopInterceptRequestLayout(false);
}
由于LayoutManager的onLayoutChildren是空实现,所以我们以LinearLayoutManager为例展开追踪分析
// LinearLayoutManager#onLayoutChildren
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
...
// 这里Recyclerview这个组件考虑了RecyclerView被回收的状态重新显示
if (mPendingSavedState != null || mPendingScrollPosition != RecyclerView.NO_POSITION) {
if (state.getItemCount() == 0) {
removeAndRecycleAllViews(recycler);
return;
}
}
if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) {
mPendingScrollPosition = mPendingSavedState.mAnchorPosition;
}
// 这个方法创建了LayoutState对象
// LayoutState对象存储了LayoutManager在当时滑动状态下的关键信息
// (比如mAvailable屏幕还剩下多少可用空间,mCurrentPosition当前已经摆放到第几个item,mScrollingOffset当前滑动的偏移量等)
// 当向列表填充item时,就会根据这个对象信息来判断
ensureLayoutState();
mLayoutState.mRecycle = false;
// resolve layout direction
// 反转布局:比如qq聊天列表,总是显示在最底下聊天的最新消息
resolveShouldLayoutReverse();
final View focused = getFocusedChild();
// AnchorInfo布局是锚点View,第一次的时候,mAnchorInfo.mValid为false,会进入if分支
if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION
|| mPendingSavedState != null) {
mAnchorInfo.reset();
mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
// calculate anchor position and coordinate
// 收集锚点View的坐标和position信息
// 锚点View可能是从列表恢复数据中取得当时的锚点
// 或者是获取了焦点的item的View
// 不然就是列表的第一个item
// 锚点View的作用是填充布局时会先从锚点位置从上往下填充,再从锚点位置从下往上填充,这是LinearLayoutManager填充的策略
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
mAnchorInfo.mValid = true;
} else if (focused != null && (mOrientationHelper.getDecoratedStart(focused)
>= mOrientationHelper.getEndAfterPadding()
|| mOrientationHelper.getDecoratedEnd(focused)
<= mOrientationHelper.getStartAfterPadding())) {
// This case relates to when the anchor child is the focused view and due to layout
// shrinking the focused view fell outside the viewport, e.g. when soft keyboard shows
// up after tapping an EditText which shrinks RV causing the focused view (The tapped
// EditText which is the anchor child) to get kicked out of the screen. Will update the
// anchor coordinate in order to make sure that the focused view is laid out. Otherwise,
// the available space in layoutState will be calculated as negative preventing the
// focused view from being laid out in fill.
// Note that we won't update the anchor position between layout passes (refer to
// TestResizingRelayoutWithAutoMeasure), which happens if we were to call
// updateAnchorInfoForLayout for an anchor that's not the focused view (e.g. a reference
// child which can change between layout passes).
mAnchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused));
}
// LLM may decide to layout items for "extra" pixels to account for scrolling target,
// caching or predictive animations.
// 在刚开始布局时就调用了LayoutManager#scrollToPosition方法所以下面会计算偏移量
...
// 重点,在布局开始前先分门别类的进行ViewHolder的回收
// 即存储到Recycler的各个集合中,目的是在布局开始时能在Recycler中复用ViewHolder
// 这里就需要了解Recycler这个类了,可以先跳到下面的Recycler类追踪,追踪完再回退到这里继续分析
// 为什么在布局前要先回收,是因为notifyDataSetChanged等方法都会调用到onLayoutChildren方法
// 因为当RecyclerView填充Item的时候都会从Recycler中获取是否有可复用的ViewHolder,所以在复用前要先回收,当复用时就从回收了的Recycler中取。
detachAndScrapAttachedViews(recycler);
...
// 回收完,开始布局流程
// 这里是判断是否要倒序布局,一般为false
if (mAnchorInfo.mLayoutFromEnd) {
...
} else {
// 这里会先从锚点从上往下,再从锚点从下往上,这两种都是调用fill方法进行填充,重点分析fill方法
// fill towards end
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForEnd;
// 重点,追踪下面分析的fill方法流程
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.mExtraFillSpace = extraForStart;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
// 重点,追踪下面分析的fill方法流程
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.mExtraFillSpace = extraForEnd;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
}
}
...
}
Recycler类的介绍,处理四级缓存的类,也是RecyclerView的重点类
public final class Recycler {
//#1 mAttachedScrap,mChangedScrap同处一级缓存,复用时不需要重新bindViewHolder绑定数据
ArrayList<ViewHolder> mAttachedScrap;
ArrayList<ViewHolder> mChangedScrap;
//#2 mCachedViews为二级缓存,随着上下滑动被滑出去的item对应的ViewHolder就会存储在这里
// 当再次滑进屏幕的时候也是不需要重新bindViewHolder绑定数据的,可通过setItemCacheSize调整,默认大小为2,
// 也有情况是当一直下滑时,复用了mCachedViews中的ViewHolder,此时则需要重新绑定数据
ArrayList<ViewHolder> mCachedViews;
//#3 mViewCacheExtension三级缓存,属于扩展,开发者自定义拓展View缓存
ViewCacheExtension mViewCacheExtension;
//#4 mRecyclerPool为四级缓存,当mCachedViews即二级缓存放不下的情况下才会放到这里,根据viewType存取ViewHolder,
// 可通过setRecycledViewPool调整,每个类型容量默认为5
RecycledViewPool mRecyclerPool;
}
fill方法流程分析
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
...
int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
// 开启一个循环,判断当前方向上是否有剩余空间可以填充item
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
...
// 循环里调用layoutChunk方法填充item
layoutChunk(recycler, state, layoutState, layoutChunkResult);
...
}
...
}
// layoutChunk方法
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
// 从recycler里面获取View,这个方法是复用机制的开始,可以在下面查看复用流程的流程图
View view = layoutState.next(recycler);
...
if (layoutState.mScrapList == null) {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
// 会调用layoutmanager的addView方法,判断该view对应的viewholder是否是即将被删除的View
// 如果是则加入到即将被删除的集合中,如果这个view之前已经添加到recyclerview中,则无需再次添加,否则就添加到recyclerview上
addView(view);
} else {
addView(view, 0);
}
} else {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addDisappearingView(view);
} else {
addDisappearingView(view, 0);
}
}
// 测量child的上下左右margin
measureChildWithMargins(view, 0, 0);
...
// We calculate everything with View's bounding box (which includes decor and margins)
// To calculate correct layout position, we subtract margins.
// 把子View摆放到合适的位置上,会考虑到ItemDecoration的情况
layoutDecoratedWithMargins(view, left, top, right, bottom);
...
}
Recycler的复用机制
总体概括:
- layoutmanager向列表填充item时,都会向Recycler索取ViewHolder
- 在索取时,Recycler先从一级缓存即mAttachedScrap和mChangedScrap中查找是否有可复用的ViewHolder,它们是屏幕内的ViewHolder,不需要重新绑定数据
- 如果在一级缓存找不到,则会在mCacheViews二级缓存中找,如果找到了,还要进行位置的判断,如果位置不变,则表示刚滑出屏幕的被滑回来了,不需要重新绑定数据,否则则比如是向上滑的item需要使用复用的ViewHolder,这种情况需要重新绑定数据
- 如果二级缓存找不到,则需要在三级缓存也就是开发这自定义的扩展mViewCacheExt中查找(很少被使用).
- 最后都找不到,只能在四级缓存mRecyclerPool里查找,在RecyclerPool里是根据viewType去查找,如果有则返回,没有就会调用Adapter类的onCreateViewHolder去创建ViewHolder并返回
上面是复用机制的总体概括,下面进行代码的分析
// 复用的阶段,最终会调用Recycler#tryGetViewHolderForPositionByDeadline方法
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
...
ViewHolder holder = null;
// 0) If there is a changed scrap, try to find from there
// 先从mChangedScrap中获取ViewHolder
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
// 1) Find by position from scrap/hidden list/cache
// 这里的顺序:
// 从mAttachedScrap中获取,如果有则返回
// 没有则调用childHelper#findHiddenNonRemovedView方法,从正在做删除动画的集合中找是否有满足复用的viewHolder
// 还没找到则从mCachedViews中查找
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if (holder != null) {
// 如果获取的ViewHolder不为空
// 就会判断获取的ViewHolder的viewtype和需要填充的item的viewtype是否一致
// 判断是否存在hasStableId身份唯一标识,只有都符合才复用
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) {
...
// 如果holder仍为null,去mViewCacheExtension中查找,一般mViewCacheExtension为null,不会进入这个if
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");
}
// 最后还找不到才去RecyclerPool中查找
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;
}
// 实在在缓存中找不到,调用createHolder方法进行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);
if (DEBUG) {
Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder");
}
}
}
...
}
到这里RecyclerView的测量,布局,复用就分析到一段落了。
小结:
- RecyclerView是一种插拔式的设计模式
- 尽量指定RecyclerView和Item的宽和高,否则在onMeasure的过程中,有可能会调用两次dispatchLayoutStep2()进行测量
- 尽量使用定向刷新notifyItemChanged,而不是notifyDataSetChanged()