RecyclerView流程学习


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就是之前说过的LayoutManager
    if (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 START
    return;
}

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
    //重点,尝试执行layoutChildren
    mState.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发生变化的时候,仍然需要重新onLayout
        if (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_START
        dispatchLayoutStep3();
    }

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;
    }
    // ······
    // 移除回收子view
    detachAndScrapAttachedViews(recycler);
    mLayoutState.mIsPreLayout = state.isPreLayout();
    // 开始填充
    if (mAnchorInfo.mLayoutFromEnd) {
        // 向开始填充,更新LayoutState
        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;
        }
        // 向末端填充
        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 start
            extraForStart = 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 end
            updateLayoutStateToFillEnd(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<MessageViewHolder> {
    @Override
    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
		... ...
	}
	@Override
    public 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。
     * ...
     */
    @Nullable
    ViewHolder 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<ViewHolder> mScrapHeap = new ArrayList<>();
        int mMaxScrap = DEFAULT_MAX_SCRAP;
        long mCreateRunningAverageNs = 0;
        long mBindRunningAverageNs = 0;
    }
    SparseArray<ScrapData> mScrap = new SparseArray<>();
    ...
}

预加载

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

滚动和fling

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值