RecyclerView流程学习
创始人
2024-06-03 12:34:04

RecyclerView流程学习

  • 模块划分
  • 绘制流程
    • onMeasure
      • mLayout为null
      • mLayout开启自动测量
      • 未开启自动测量
    • onLayout
    • onDraw
    • onLayoutChildren
  • 缓存
  • 预加载
  • 滚动和fling

RecyclerView

模块划分

RecyclerView中根据其功能可以分为以下几个模块:

  1. Recycler mRecycler // 缓存管理者,final类型-不允许扩展
  2. LayoutManager mLayoutManager // 数据展示者
  3. RecyclerViewDataObserver mObserver // 数据观察者
  4. Adapter mAdapter // 数据提供者
  5. ItemAnimator mItemAnimator // 动画类

绘制流程

onMeasure

protected void onMeasure(int widthSpec, int heightSpec) {//mLayout就是之前说过的LayoutManagerif (mLayout == null) {// 第一种情况}if (mLayout.isAutoMeasureEnabled()) {// 第二种情况} else {// 第三种情况}
}

在onMeasure方法中,recyclerView根据mLayout分为了三种情况,接下来会对这三种情况进行梳理。

mLayout为null

第一种,当mLayout为null的时候:

if (mLayout == null) {defaultOnMeasure(widthSpec, heightSpec);return;
}
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);
}
public static int chooseSize(int spec, int desired, int min) {final int mode = View.MeasureSpec.getMode(spec);final int size = View.MeasureSpec.getSize(spec);switch (mode) {//表示精确模式,View的大小已经确认,为SpecSize所指定的值case View.MeasureSpec.EXACTLY:return size;//指定了最大大小case View.MeasureSpec.AT_MOST:return Math.min(size, Math.max(desired, min));//父容器不对子View有限制,子View要多大给多大case View.MeasureSpec.UNSPECIFIED:default:return Math.max(desired, min);}
}

可以看到在mLayout为Null的情况下,recyclerView还是做了测量操作,但是由于在onLayout方法中跳过了layout,因此不会展示任何东西。

if (mLayout == null) {Log.e(TAG, "No layout manager attached; skipping layout");// leave the state in STARTreturn;
}

mLayout开启自动测量

在这种情况下,RecyclerView会先后调用dispatchLayoutStep1()dispatchLayoutStep2()方法,除此之外,还有一个dispatchLayoutStep3()方法会在onLayout中进行调用,这三个方法对应了RecyclerView的三种不同状态,State.STEP_START、State.STEP_LAYOUT和State.STEP_ANIMATIONS。
State.STEP_START表示RecyclerView还未经历dispatchLayoutStep1()
State.STEP_LAYOUT表示此时处于layout阶段,这个阶段会调用dispatchLayoutStep2方法layout RecyclerView的children。调用dispatchLayoutStep2方法之后,此时mState.mLayoutStep变为了State.STEP_ANIMATIONS。
当mState.mLayoutStep为State.STEP_ANIMATIONS时,表示RecyclerView处于第三个阶段,也就是执行动画的阶段,也就是调用dispatchLayoutStep3方法。当dispatchLayoutStep3方法执行完毕之后,mState.mLayoutStep又变为了State.STEP_START。
具体地,先来看一下源码:

if (mLayout.isAutoMeasureEnabled()) {final int widthMode = MeasureSpec.getMode(widthSpec);final int heightMode = MeasureSpec.getMode(heightSpec);//官方说明:这里的调用已经废弃,事实上已经替换为defaultOnMeasure//也就是说mLayout应该调用defaultOnMeasure()方法//但是为了防止第三方代码被破坏,还是保留了下来//所有开发人员在isAutoMeasureEnabled为true不应该覆盖onMeasure方法mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);final boolean measureSpecModeIsExactly =widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;if (measureSpecModeIsExactly || mAdapter == null) {return;}if (mState.mLayoutStep == State.STEP_START) {dispatchLayoutStep1();}// 在第 2 步中设置尺寸。 为了保持一致性,应该使用旧尺寸进行预布局mLayout.setMeasureSpecs(widthSpec, heightSpec);mState.mIsMeasuring = true;dispatchLayoutStep2();// 现在我们可以从孩子那里得到宽度和高度.mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);// 如果 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);}
}

可以看到这里最终还是调用了defaultOnMeasure()这个方法在之前进行了介绍,但是与之前不同的一点是在此之后又调用了dispatchLayoutStep1()dispatchLayoutStep2(),并且如果需要二次测量的话会重新执行
dispatchLayoutStep2()
那么这两个方法具体起到了什么作用呢?
先来看一下这两个方法的源码:

private void dispatchLayoutStep1() {... ...processAdapterUpdatesAndSetAnimationFlags();... ...if (mState.mRunSimpleAnimations) {// 找到没有被remove的ItemView,保存OldViewHolder信息,准备预布局}if (mState.mRunPredictiveAnimations) {// 进行预布局} else {clearOldPositions();}onExitLayoutOrScroll();stopInterceptRequestLayout(false);mState.mLayoutStep = State.STEP_LAYOUT;
}private void processAdapterUpdatesAndSetAnimationFlags() {if (mDataSetHasChangedAfterLayout) {// 因为数据集意外更改,处理这些项目没有价值。 // 相反,我们只是重置它。mAdapterHelper.reset();if (mDispatchItemsChangedEvent) {mLayout.onItemsChanged(this);}}// 简单动画是高级动画的一个子集(这将导致预布局步骤)如果布局支持预测动画,// 则进行预处理以决定我们是否要运行它们if (predictiveItemAnimationsEnabled()) {mAdapterHelper.preProcess();} else {mAdapterHelper.consumeUpdatesInOnePass();}boolean animationTypeSupported = mItemsAddedOrRemoved || mItemsChanged;//重点,这里主要设置了mRunSimpleAnimations和mRunPredictiveAnimations的值//mFirstLayoutComplete是指第一次绘制流程完成,当未完成时为false//因此当一次绘制的时候mRunSimpleAnimations和mRunPredictiveAnimations都为false//不会加载动画mState.mRunSimpleAnimations = mFirstLayoutComplete&& mItemAnimator != null&& (mDataSetHasChangedAfterLayout|| animationTypeSupported|| mLayout.mRequestedSimpleAnimations)&& (!mDataSetHasChangedAfterLayout|| mAdapter.hasStableIds());mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations&& animationTypeSupported&& !mDataSetHasChangedAfterLayout&& predictiveItemAnimationsEnabled();
}private void dispatchLayoutStep2() {...mState.mItemCount = mAdapter.getItemCount();mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;// Step 2: Run layout//重点,尝试执行layoutChildrenmState.mInPreLayout = false;mLayout.onLayoutChildren(mRecycler, mState);...mState.mLayoutStep = State.STEP_ANIMATIONS;...
}

总结一下,step1的功能是与Animations有关,控制是否加载动画,而step2的功能是尝试layoutChildren。

未开启自动测量

if (mHasFixedSize) {mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);return;
}
// custom onMeasure
if (mAdapterUpdateDuringMeasure) {eatRequestLayout();processAdapterUpdatesAndSetAnimationFlags();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;resumeRequestLayout(false);
}if (mAdapter != null) {mState.mItemCount = mAdapter.getItemCount();
} else {mState.mItemCount = 0;
}
eatRequestLayout();
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
resumeRequestLayout(false);
mState.mInPreLayout = false; // clear

如果mHasFixedSize为true,就直接调用LayoutManager.onMeasure方法进行测量,如果mHasFixedSize为false,则先判断是否有数据更新,有的话先处理数据更新,再调用LayoutManager.onMeasure方法进行测量。

onLayout

onMeasure方法完成之后,接下来应该进行onLayout方法:

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);dispatchLayout();TraceCompat.endSection();mFirstLayoutComplete = true;
}

这里最重要的就是dispatchLayout方法,来具体地看一下这个方法的实现:

    void dispatchLayout() {... ...mState.mIsMeasuring = false;//前两步已经在onMeasure的时候完成,//但是当size发生变化的时候,仍然需要重新onLayoutif (mState.mLayoutStep == State.STEP_START) {dispatchLayoutStep1();mLayout.setExactMeasureSpecsFrom(this);dispatchLayoutStep2();} else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()|| mLayout.getHeight() != getHeight()) {mLayout.setExactMeasureSpecsFrom(this);dispatchLayoutStep2();} else {// always make sure we sync them (to ensure mode is exact)mLayout.setExactMeasureSpecsFrom(this);}//布局的最后一步,我们保存有关动画视图的信息,触发动画并进行任何必要的清理。//这里设置了mState.mLayoutStep = State.STEP_STARTdispatchLayoutStep3();}

onDraw

public void draw(Canvas c) {super.draw(c);final int count = mItemDecorations.size();//调用了mItemDecorations的onDraw方法//此时item已经在绘制了,这意味着ItemDecoration会在item上方被绘制for (int i = 0; i < count; i++) {mItemDecorations.get(i).onDrawOver(c, this, mState);}... ...
}
@Override
public void onDraw(Canvas c) {super.onDraw(c);//调用了mItemDecorations的onDraw方法//此时item还没有绘制,这意味着ItemDecoration会在item下方被绘制final int count = mItemDecorations.size();for (int i = 0; i < count; i++) {mItemDecorations.get(i).onDraw(c, this, mState);}
}

这里有两个知识要点:

  1. 当view draw的时候会以适当执行以下方法(但是3一定会在4前面):
    1. 绘制背景
    2. 如有必要,保存画布的图层以备褪色
    3. 绘制视图的内容(调用onDraw()方法)
    4. 画孩子(dispatchDraw() 将draw事件分发给children)
    5. 如有必要,绘制褪色边缘并恢复图层
    6. 绘制装饰(例如滚动条)
    7. 如有必要,绘制默认焦点高亮
  2. itemDecoration是指item的装饰,系统默认实现了一个DividerItemDecoration用作item之间的分割线。实际上,itemDecoration可以实现更多的效果,这里可以参考这篇文章RecyclerView系列之二ItemDecoration。

总而言之draw方法主要做了以下几件事情:

  • 将draw事件分发给子类
  • 绘制itemDecoration
  • 根据setClipToPadding制定特殊的滑动效果

onLayoutChildren

在之前的dispatchLayoutStep2中,RecyclerView调用了mLayout.onLayoutChildren,LayoutManager是具体的item展示者,由它来确定item应该如何展示,如何布局。具体的应用可以查看这篇文章:LayoutManager及其自定义。
这里只讲述LayoutManager是如何绘制item的,以LinearLayoutManager为例:

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {//布局算法:// 1) 通过检查children和其他变量,找到锚点坐标和锚点项位置。// 2) 向开始填充,从底部堆叠// 3) 向末端填充,从顶部堆叠// 4) 滚动以满足从底部堆叠的要求。// 创建布局状态// ······// 第一步final View focused = getFocusedChild();if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION|| mPendingSavedState != null) {mAnchorInfo.reset();mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;// 计算锚点的位置和坐标updateAnchorInfoForLayout(recycler, state, mAnchorInfo);mAnchorInfo.mValid = true;}// ······// 移除回收子viewdetachAndScrapAttachedViews(recycler);mLayoutState.mIsPreLayout = state.isPreLayout();// 开始填充if (mAnchorInfo.mLayoutFromEnd) {// 向开始填充,更新LayoutStateupdateLayoutStateToFillStart(mAnchorInfo);mLayoutState.mExtra = extraForStart;fill(recycler, mLayoutState, state, false);startOffset = mLayoutState.mOffset;final int firstElement = mLayoutState.mCurrentPosition;if (mLayoutState.mAvailable > 0) {extraForEnd += mLayoutState.mAvailable;}// 向末端填充updateLayoutStateToFillEnd(mAnchorInfo);mLayoutState.mExtra = extraForEnd;mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;fill(recycler, mLayoutState, state, false);endOffset = mLayoutState.mOffset;if (mLayoutState.mAvailable > 0) {// end could not consume all. add more items towards startextraForStart = mLayoutState.mAvailable;updateLayoutStateToFillStart(firstElement, startOffset);mLayoutState.mExtra = extraForStart;fill(recycler, mLayoutState, state, false);startOffset = mLayoutState.mOffset;}} else {// 向开始填充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;}// 向末端填充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 endupdateLayoutStateToFillEnd(lastElement, endOffset);mLayoutState.mExtra = extraForEnd;fill(recycler, mLayoutState, state, false);endOffset = mLayoutState.mOffset;}}// ······
}

可以看到在这个方法中,首先计算出了锚点的位置和坐标,然后以锚点开始向start方向或者end方向进行填充,如果还有剩余位置的话就从另一个方向开始进行填充。
简单总结一下整个流程就是:

确定锚点更新layoutState根据layoutState和填充方向开始fill向另外一个方向填充

fill方法中,真正填充的方法是layoutChunk

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,RecyclerView.State state, boolean stopOnFocusable) {// ······while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {// ······layoutChunk(recycler, state, layoutState, layoutChunkResult);}// ······
}

layoutChunk的执行流程如下:

调用LayoutState的next方法获得一个ItemView。千万别小看这个next方法,RecyclerView缓存机制的起点就是从这个方法开始,可想而知,这个方法到底为我们做了多少事情。
如果RecyclerView是第一次布局Children的话(layoutState.mScrapList == null为true),会先调用addView,将View添加到RecyclerView里面去。
调用measureChildWithMargins方法,测量每个ItemView的宽高。注意这个方法测量ItemView的宽高考虑到了两个因素:1.margin属性;2.ItemDecoration的offset。
调用layoutDecoratedWithMargins方法,布局ItemView。这里也考虑上面的两个因素的。

缓存

在讲述缓存之前,不妨先思考一下,为什么RecyclerView需要缓存机制?
在RecyclerView的Adapter中,对于不同的数据类型会为其创建一个ViewHolder,并且将数据绑定到这个ViewHolder上。

public class MyAdapter extends RecyclerView.Adapter {@Overridepublic MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){... ...}@Overridepublic void onBindViewHolder(@NonNull MyViewHolder holder, int position){... ...}
}

但是无论是onCreateViewHolder还是onBindViewHolder都会使用到findViewById方法,如果每次一个新item进入都需要回调这两个方法会导致效率很低,因此需要对ViewHolder进行缓存复用以减少回调频次。

先看看这篇文章吧
这10张图拿去,别再说学不会RecyclerView的缓存复用机制了!

public final class Recycler {.../*** Attempts to get the ViewHolder for the given position, either from the Recycler scrap,* cache, the RecycledViewPool, or creating it directly.* * 尝试通过从Recycler scrap缓存、RecycledViewPool查找或直接创建的形式来获取指定位置的ViewHolder。* ...*/@NullableViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) {if (mState.isPreLayout()) {// 0 尝试从mChangedScrap中获取ViewHolder对象holder = getChangedScrapViewForPosition(position);...}if (holder == null) {// 1.1 尝试根据position从mAttachedScrap或mCachedViews中获取ViewHolder对象holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);...}if (holder == null) {...final int type = mAdapter.getItemViewType(offsetPosition);if (mAdapter.hasStableIds()) {// 1.2 尝试根据id从mAttachedScrap或mCachedViews中获取ViewHolder对象holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),type, dryRun);...}if (holder == null && mViewCacheExtension != null) {// 2 尝试从mViewCacheExtension中获取ViewHolder对象final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);if (view != null) {holder = getChildViewHolder(view);...}}if (holder == null) { // fallback to pool// 3 尝试从mRecycledViewPool中获取ViewHolder对象holder = getRecycledViewPool().getRecycledView(type);...}if (holder == null) {// 4.1 回调createViewHolder方法创建ViewHolder对象及其关联的视图holder = mAdapter.createViewHolder(RecyclerView.this, type);...}}if (mState.isPreLayout() && holder.isBound()) {...} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {...// 4.1 回调bindViewHolder方法提取数据填充ViewHolder的视图内容bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);}...return holder;}...
}    

从上述代码,可以看到RecyclerView在尝试获取ViewHolder的时候会依次从mChangedScrap、mAttachedScrap、mCachedViews、mViewCacheExtension、mRecycledViewPool获取,如果都获取不到才会创建viewHolder。
mChangedScrap、mAttachedScrap主要用于存放当前屏幕可见,但是被标记为“移除”或者“重用”的列表项。
mChangedScrap主要用于notifyItemChanged、notifyItemRangeChanged这类方法在动画开启下的场景,而mAttachedScrap则用于notifyItemMoved、notifyItemRemoved这类列表项发生移动的场景。
当调用notifyItemRemoved后,RecyclerView首先会将所有的可见的viewHolder加入到mAttachedScrap中,等到重新布局完成,开始展示子视图之后再遍历mAttachedScrap找到对应position的viewHolder。

mCachedViews用于存放已经被移除屏幕,但是很快有可能进入屏幕的列表项,默认大小为2。这里比较好理解,假设RecyclerView向下滑动,当最上面的viewHolder滚动出可见区的时候,这个viewHolder会加入到mCachedViews中。之后如果反方向滑动,当最上面的viewHolder再次进入到可见区域之后,就会尝试从mCachedViews再次获取之前的viewHolder。

mViewCacheExtension主要用于提供额外的、开发人员使用的缓冲区。

mRecyclerPool主要用于按不同的itemType分别存放超出mCachedViews限制的、被移出屏幕的列表项。

public static class RecycledViewPool {private static final int DEFAULT_MAX_SCRAP = 5;//自定义了数据类型ScrapData,每个ScrapData内部存有最多5个同一viewType类型的viewHolder。static class ScrapData {final ArrayList mScrapHeap = new ArrayList<>();int mMaxScrap = DEFAULT_MAX_SCRAP;long mCreateRunningAverageNs = 0;long mBindRunningAverageNs = 0;}SparseArray mScrap = new SparseArray<>();...
}

预加载

先看看这篇文章吧
掌握这17张图,没人比你更懂RecyclerView的预加载

滚动和fling

RecyclerView 的滚动是怎么实现的?(一)
RecyclerView 的滚动时怎么实现的?(二)| Fling

简单来说,RecyclerView回去捕获MotionEvent.ACTION_MOVE事件,并且计算出移动的距离。
当移动超过一定距离则进入Fling状态,每一帧都会去执行移动任务。
当计算出移动距离之后,委托LayoutManager执行当前的item向反方向移动的动画,并且填充空闲区域。

相关内容

热门资讯

华凯易佰涨8.81%,成交额7... 1月12日,华凯易佰涨8.81%,成交额7.07亿元,换手率15.62%,总市值53.46亿元。异动...
美康生物涨0.88%,成交额4... 1月12日,美康生物涨0.88%,成交额4951.34万元,换手率1.65%,总市值39.51亿元。...
康弘药业涨0.17%,成交额5... 1月12日,康弘药业涨0.17%,成交额5.34亿元,换手率2.26%,总市值316.84亿元。异动...
国信证券涨0.52%,成交额6... 1月12日,国信证券涨0.52%,成交额6.27亿元,换手率0.49%,总市值1384.68亿元。异...
开立医疗涨1.66%,成交额1... 1月12日,开立医疗涨1.66%,成交额1.58亿元,换手率1.25%,总市值127.43亿元。异动...