RecycleView总结

本文打算从几个地方说起。

  • RecyclerView当成一个普通的View,分别分析它的三大流程、事件传递(包括嵌套滑动)
  • 分析RecyclerView的缓存原理,这也是RecyclerView的精华所在
  • 分析RecyclerViewAdapterLayoutManagerItemAnimatorItemDecoration
  • RecyclerView的扩展,包括LayoutManager的自定义和使用RecyclerView常见的坑

一.Recycleview绘制三大流程:

1.概述:

RecyclerView本身是一个展示大量数据的控件,相比较ListView,RecyclerView的4级缓存(也有人说是3级缓存,这些都不重要)就表现的非常出色,在性能方面相比于ListView提升了不少。同时由于LayoutManager的存在,让RecyclerView不仅有ListView的特点,同时兼有GridView的特点。这可能是RecyclerView受欢迎的原因之一吧。

RecyclerView在设计方面上也是非常的灵活,不同的部分承担着不同的职责。其中Adapter负责提供数据,包括创建ViewHolder和绑定数据,LayoutManager负责ItemView的测量和布局,ItemAnimator负责每个ItemView的动画,ItemDecoration负责每个ItemView的间隙。这种插拔式的架构使得RecyclerView变得非常的灵活,每一个人都可以根据自身的需求来定义不同的部分。

本文RecyclerView源码均来自于27.1.1

2.measure

来看看RecyclerViewonMeasure方法。

protected void onMeasure(int widthSpec, int heightSpec) {
        if (mLayout == null) {
            // 第一种情况
        }
        if (mLayout.isAutoMeasureEnabled()) {
            // 第二种情况
        } else {// 第三种情况}
}

onMeasure方法还是有点长,这里我将它分为3种情况,我将简单解释这三种情况。

  1. mLayoutLayoutManager的对象。我们知道,当RecyclerViewLayoutManager为空时,RecyclerView不能显示任何的数据,在这里我们找到答案。
  2. LayoutManager开启了自动测量时,这是一种情况。在这种情况下,有可能会测量两次。
  3. 第三种情况就是没有开启自动测量的情况,这种情况比较少,因为为了RecyclerView支持warp_content属性,系统提供的LayoutManager都开启自动测量的,不过我们还是要分析的。

(1)当LayoutManager为空时:

这种情况下比较简单:

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);
    }

defaultOnMeasure方法里面,先是通过LayoutManagerchooseSize方法来计算值,然后就是setMeasuredDimension方法来设置宽高。

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) {
                case View.MeasureSpec.EXACTLY:
                    return size;
                case View.MeasureSpec.AT_MOST:
                    return Math.min(size, Math.max(desired, min));
                case View.MeasureSpec.UNSPECIFIED:
                default:
                    return Math.max(desired, min);
            }
        }

chooseSize方法表达的意思比较简单,就是通过RecyclerView的测量mode来获取不同的值,测量模式分为三种,就是我们常见的view绘制中measure的三种模式。

到此,第一种情况就分析完毕了。因为当LayoutManager为空时,那么当RecyclerView处于onLayout阶段时,会调用dispatchLayout方法。而在dispatchLayout方法里面有这么一行代码:

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

所以,当LayoutManager为空时,不显示任何数据是理所当然的。

(2)当LayoutManager开启了自动测量

在分析这种情况之前,我们得先了解几个东西。

RecyclerView的测量分为两步,分别调用dispatchLayoutStep1dispatchLayoutStep2。同时,了解过RecyclerView源码的同学应该知道在RecyclerView的源码里面还一个dispatchLayoutStep3方法。这三个方法的方法名比较接近,所以容易让人搞混淆。本文会详细的讲解这三个方法的作用。

由于在这种情况下,只会调用dispatchLayoutStep1dispatchLayoutStep2这两个方法,所以这里会重点的讲解这两个方法。而dispatchLayoutStep3方法的调用在RecyclerViewonLayout方法里面,所以在后面分析onLayout方法时再来看dispatchLayoutStep3方法。
  我们在分析之前,先来看一个东西--mState.mLayoutStep。这个变量有几个取值情况。我们分别来看看:

取值

含义

State.STEP_START

mState.mLayoutStep的默认值,这种情况下,表示RecyclerView还未经历dispatchLayoutStep1,因为dispatchLayoutStep1调用之后mState.mLayoutStep会变为State.STEP_LAYOUT

State.STEP_LAYOUT

mState.mLayoutStepState.STEP_LAYOUT时,表示此时处于layout阶段,这个阶段会调用dispatchLayoutStep2方法layout RecyclerViewchildren。调用dispatchLayoutStep2方法之后,此时mState.mLayoutStep变为了State.STEP_ANIMATIONS

State.STEP_ANIMATIONS

mState.mLayoutStepState.STEP_ANIMATIONS时,表示RecyclerView处于第三个阶段,也就是执行动画的阶段,也就是调用dispatchLayoutStep3方法。当dispatchLayoutStep3方法执行完毕之后,mState.mLayoutStep又变为了State.STEP_START

从上表中,我们了解到mState.mLayoutStep的三个状态对应着不同的dispatchLayoutStep方法。这一点,我们必须清楚,否则接下来的代码将难以理解。

好的,现在开始正式分析源码:

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.
             */
            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();
            }
            // set dimensions in 2nd step. Pre-layout should happen with old dimensions for
            // consistency
            mLayout.setMeasureSpecs(widthSpec, heightSpec);
            mState.mIsMeasuring = true;
            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.
            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);
            }
        }

我将这段代码分为三步:

  1. 调用LayoutManageronMeasure方法进行测量。对于onMeasure方法,我也感觉到非常的迷惑,发现传统的LayoutManager都没有实现这个方法。后面,我们会将简单的看一下这个方法。
  2. 如果mState.mLayoutStepState.STEP_START的话,那么就会执行dispatchLayoutStep1方法,然后会执行dispatchLayoutStep2方法。
  3. 如果需要第二次测量的话,会再一次调用dispatchLayoutStep2 方法。

首先,我们来看看第一步,也就是看看onMeasure方法。LayoutManageronMeasure方法究竟为我们做什么,我们来看看:

public void onMeasure(Recycler recycler, State state, int widthSpec, int heightSpec) {
            mRecyclerView.defaultOnMeasure(widthSpec, heightSpec);
}

默认是调用的RecyclerViewdefaultOnMeasure方法,至于defaultOnMeasure方法里面究竟做了什么,这在前面已经介绍过了,这里就不再介绍了。

ViewonMeasure方法的作用通常来说有两个。一是测量自身的宽高,从RecyclerView来看,它将自己的测量工作托管给了LayoutManageronMeasure方法。所以,我们在自定义LayoutManager时,需要注意onMeasure方法存在,不过从官方提供的几个LayoutManager,都没有重写这个方法。所以不到万得已,最好不要重写LayoutManageronMeasure方法;二是测量子View,不过到这里我们还没有看到具体的实现。

接下来,我们来分析第二步,看看dispatchLayoutStep1方法和dispatchLayoutStep2方法究竟做了什么。
在正式分析第二步之前,我们先对这三个方法有一个大概的认识。

方法名

作用

dispatchLayoutStep1

三大dispatchLayoutStep方法第一步。本方法的作用主要有三点:1.处理Adapter更新;2.决定是否执行ItemAnimator;3.保存ItemView的动画信息。本方法也被称为preLayout(预布局),当Adapter更新了,这个方法会保存每个ItemView的旧信息(oldViewHolderInfo)

dispatchLayoutStep2三大dispatchLayoutStep方法第二步。在这个方法里面,真正进行children的测量和布局。
dispatchLayoutStep3

三大dispatchLayoutStep方法第三步。这个方法的作用执行在dispatchLayoutStep1方法里面保存的动画信息。本方法不是本文的介绍重点,后面在介绍ItemAnimator时,会重点分析这个方法。

我们回到recycleview的onMeasure方法里面,先看看整个执行过程。

if (mState.mLayoutStep == State.STEP_START) {
      dispatchLayoutStep1();
}
// set dimensions in 2nd step. Pre-layout should happen with old dimensions for
// consistency
mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
dispatchLayoutStep2();

如果mState.mLayoutStep == State.STEP_START时,才会调用 dispatchLayoutStep1方法,这里与我们前面介绍mLayoutStep对应起来了。现在我们看看dispatchLayoutStep1方法:

    private void dispatchLayoutStep1() {
        mState.assertLayoutStep(State.STEP_START);
        fillRemainingScrollValues(mState);
        mState.mIsMeasuring = false;
        startInterceptRequestLayout();
        mViewInfoStore.clear();
        onEnterLayoutOrScroll();
        processAdapterUpdatesAndSetAnimationFlags();
        saveFocusInfo();
        mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged;
        mItemsAddedOrRemoved = mItemsChanged = false;
        mState.mInPreLayout = mState.mRunPredictiveAnimations;
        mState.mItemCount = mAdapter.getItemCount();
        findMinMaxChildLayoutPositions(mMinMaxLayoutPositions);

        if (mState.mRunSimpleAnimations) {
           // 找到没有被remove的ItemView,保存OldViewHolder信息,准备预布局
        }
        if (mState.mRunPredictiveAnimations) {
           // 进行预布局
        } else {
            clearOldPositions();
        }
        onExitLayoutOrScroll();
        stopInterceptRequestLayout(false);
        mState.mLayoutStep = State.STEP_LAYOUT;
    }

这里先简单分析一下这个方法,因为这个方法跟ItemAnimator有莫大的关系,后续在介绍ItemAnimator时会详细的分析。在这里,我们将重点放在processAdapterUpdatesAndSetAnimationFlags里面,因为这个方法计算了mRunSimpleAnimationsmRunPredictiveAnimations

    private void processAdapterUpdatesAndSetAnimationFlags() {
        if (mDataSetHasChangedAfterLayout) {
            // Processing these items have no value since data set changed unexpectedly.
            // Instead, we just reset it.
            mAdapterHelper.reset();
            if (mDispatchItemsChangedEvent) {
                mLayout.onItemsChanged(this);
            }
        }
        // simple animations are a subset of advanced animations (which will cause a
        // pre-layout step)
        // If layout supports predictive animations, pre-process to decide if we want to run them
        if (predictiveItemAnimationsEnabled()) {
            mAdapterHelper.preProcess();
        } else {
            mAdapterHelper.consumeUpdatesInOnePass();
        }
        boolean animationTypeSupported = mItemsAddedOrRemoved || mItemsChanged;
        mState.mRunSimpleAnimations = mFirstLayoutComplete
                && mItemAnimator != null
                && (mDataSetHasChangedAfterLayout
                || animationTypeSupported
                || mLayout.mRequestedSimpleAnimations)
                && (!mDataSetHasChangedAfterLayout
                || mAdapter.hasStableIds());
        mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations
                && animationTypeSupported
                && !mDataSetHasChangedAfterLayout
                && predictiveItemAnimationsEnabled();
    }

这里我们的重心放在mFirstLayoutComplete变量里面,我们发现mRunSimpleAnimations的值与mFirstLayoutComplete有关,mRunPredictiveAnimations同时跟mRunSimpleAnimations有关。所以这里我们可以得出一个结论,当RecyclerView第一次加载数据时,是不会执行的动画。换句话说,每个ItemView还没有layout完毕,怎么会进行动画。

接下来我们看看dispatchLayoutStep2方法,这个方法是真正布局children。我们来看看:

    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;
        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);
    }

在这里,我们重点的看两行代码。一是在这里,我们可以看到AdaptergetItemCount方法被调用;二是调用了LayoutManageronLayoutChildren方法,这个方法里面进行对children的测量和布局,同时这个方法也是这里的分析重点。

系统的LayoutManageronLayoutChildren方法是一个空方法,所以需要LayoutManager的子类自己来实现。从这里,我们可以得出两个点。

  1. 子类LayoutManager需要自己实现onLayoutChildren方法,从而来决定RecyclerView在该LayoutManager的策略下,应该怎么布局。从这里,我们看出来RecyclerView的灵活性。
  2. LayoutManager类似于ViewGroup,将onLayoutChildren方法(ViewGrouponLayout方法)公开出来,这种模式在Android中很常见的。

这里,我先不对onLayoutChildren方法进行展开,待会会详细的分析。接下来,我们来分析第三种情况--没有开启自动测量。

(3)没有开启自动测量

先来看看这段代码:

            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

上面的代码我将分为两步:

  1. 如果mHasFixedSize为true(也就是调用了setHasFixedSize方法),将直接调用LayoutManageronMeasure方法进行测量。
  2. 如果mHasFixedSize为false,同时此时如果有数据更新,先处理数据更新的事务,然后调用LayoutManageronMeasure方法进行测量。

通过上面的描述,我们知道,如果未开启自动测量,那么肯定会调用LayoutManageronMeasure方法来进行测量,这就是LayoutManageronMeasure方法的作用。

3.layout

measure过程分析的差不多了,接下来我们就该分析第二个过程--layout。我们来看看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;
    }

onLayout方法本身没有做多少的事情,重点还是在dispatchLayout方法里面。

    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;
        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();
    }

dispatchLayout保证RecyclerView必须经历三个过程--dispatchLayoutStep1dispatchLayoutStep2dispatchLayoutStep3
  同时,在后面的文章中,你会看到dispatchLayout方法其实还为RecyclerView节省了很多步骤,也就是说,在RecyclerView经历一次完整的dispatchLayout之后,后续如果参数有所变化时,可能只会经历最后的1步或者2步。
  对于dispatchLayoutStep1dispatchLayoutStep2方法,我们前面已经讲解了,这里就不做过多的解释了。这里,我们就简单的看一下dispatchLayoutStep3方法吧。

    private void dispatchLayoutStep3() {
        // ······
        mState.mLayoutStep = State.STEP_START;
        // ······
    }

为什么这里只是简单看一下dispatchLayoutStep3方法呢?因为这个方法主要是做Item的动画,也就是我们熟知的ItemAnimator的执行,而本文不对动画进行展开,所以先省略动画部分。

在这里,我们需要关注dispatchLayoutStep3方法的是,它将mLayoutStep重置为了State.STEP_START。也就是说如果下一次重新开始dispatchLayout的话,那么肯定会经历dispatchLayoutStep1dispatchLayoutStep2dispatchLayoutStep3三个方法。
  以上就是RecyclerView的layout过程,是不是感觉非常的简单?RecyclerView跟其他ViewGroup不同的地方在于,如果开启了自动测量,在measure阶段,已经将Children布局完成了;如果没有开启自动测量,则在layout阶段才布局Children

4.draw

接下来,我们来分析三大流程的最后一个阶段--draw。在正式分析draw过程之前,我先来对RecyclerViewdraw做一个概述。
RecyclerView分为三步:

  1. 调用super.draw方法。这里主要做了两件事:1. 将Children的绘制分发给ViewGroup;2. 将分割线的绘制分发给ItemDecoration
  2. 如果需要的话,调用ItemDecorationonDrawOver方法。通过这个方法,我们在每个ItemView上面画上很多东西。
  3. 如果RecyclerView调用了setClipToPadding,会实现一种特殊的滑动效果–每个ItemView可以滑动到padding区域。
    public void draw(Canvas c) {
        // 第一步
        super.draw(c);
        // 第二步
        final int count = mItemDecorations.size();
        for (int i = 0; i < count; i++) {
            mItemDecorations.get(i).onDrawOver(c, this, mState);
        }
        // 第三步
        // TODO If padding is not 0 and clipChildrenToPadding is false, to draw glows properly, we
        // need find children closest to edges. Not sure if it is worth the effort.
        // ······
    }

熟悉三大流程的同学,肯定知道第一步会回调到onDraw方法里面,而ItemDecoration的绘制就是在onDraw方法里面。

    @Override
    public void onDraw(Canvas c) {
        super.onDraw(c);

        final int count = mItemDecorations.size();
        for (int i = 0; i < count; i++) {
            mItemDecorations.get(i).onDraw(c, this, mState);
        }
    }

onDraw方法主要是将ItemDecoration的绘制分发到ItemDecorationonDraw方法里面去。从这里,我们可以看出来,RecyclerView的设计实在是太灵活了!

5.LayoutManager的onLayoutChildren方法

       从整体来说,RecyclerView的三大流程还是比较简单,不过在整个过程中,我们似乎忽略了一个过程--那就是RecyclerView到底是怎么layout children的?
  前面在介绍dispatchLayoutStep2方法时,只是简单的介绍了,RecyclerView通过调用LayoutManageronLayoutChildren方法。LayoutManager本身对这个方法没有进行实现,所以必须得看看它的子类,这里我们就来看看LinearLayoutManager
  由于LinearLayoutManageronLayoutChildren方法比较长,这里不可能贴出完整的代码,所以这里我先对这个方法做一个简单的概述,方便理解。

  1. 确定锚点的信息,这里面的信息包括:1.Children的布局方向,有start和end两个方向;2. mPositionmCoordinate,分别表示Children开始填充的position和坐标。
  2. 调用detachAndScrapAttachedViews方法,detach掉或者removeRecyclerViewChildren。这一点本来不在本文的讲解范围内,但是为了后续对RecyclerView的缓存机制有更好的了解,这里特别的提醒一下。
  3. 根据锚点信息,调用fill方法进行Children的填充。这个过程中根据锚点信息的不同,可能会调用两次fill方法。

接下来我们来看看代码:

 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        // layout algorithm:
        // 1) by checking children and other variables, find an anchor coordinate and an anchor
        //  item position.
        // 2) fill towards start, stacking from bottom
        // 3) fill towards end, stacking from top
        // 4) scroll to fulfill requirements like stack from bottom.
        // create layout state
        // ······
        // 第一步
        final View focused = getFocusedChild();
        if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION
                || mPendingSavedState != null) {
            mAnchorInfo.reset();
            mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
            // calculate anchor position and coordinate
            updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
            mAnchorInfo.mValid = true;
        }
        // ······
        // 第二步
        detachAndScrapAttachedViews(recycler);
        mLayoutState.mIsPreLayout = state.isPreLayout();
        // 第三步
        if (mAnchorInfo.mLayoutFromEnd) {
            // fill towards start
            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;
            }
            // fill towards end
            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 {
            // fill towards end
            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;
            }
            // fill towards start
            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;
            }
        }
        // ······
    }

相信从上面的代码都可以找出每一步的执行。现在,我们来详细分析每一步。首先来看第一步--确定锚点的信息
要想看锚点信息的计算过程,我们可以从updateAnchorInfoForLayout方法里面来找出答案,我们来看看updateAnchorInfoForLayout方法:

    private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state,
            AnchorInfo anchorInfo) {
        // 第一种计算方式
        if (updateAnchorFromPendingData(state, anchorInfo)) {
            return;
        }
        // 第二种计算方式
        if (updateAnchorFromChildren(recycler, state, anchorInfo)) {
            return;
        }
        // 第三种计算方式
        anchorInfo.assignCoordinateFromPadding();
        anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
    }

简单分析一下这三种计算所做的含义:

  1. 第一种计算方式,表示含义有两种:1.RecyclerView被重建,期间回调了onSaveInstanceState方法,所以目的是为了恢复上次的布局;2.RecyclerView调用了scrollToPosition之类的方法,所以目的是让
    RecyclerView滚到准确的位置上去。所以,锚点的信息根据上面的两种情况来计算。
  2. 第二种计算方法,从Children上面来计算锚点信息。这种计算方式也有两种情况:1. 如果当前有拥有焦点的Child,那么有当前有焦点的Child的位置来计算锚点;2. 如果没有child拥有焦点,那么根据布局方向(此时布局方向由mLayoutFromEnd来决定)获取可见的第一个ItemView或者最后一个ItemView
  3. 如果前面两种方式都计算失败了,那么采用第三种计算方式,也就是默认的计算方式。

以上就是updateAnchorInfoForLayout方法所做的事情,这里就不详细纠结每种计算方式的细节。至于第二步,调用detachAndScrapAttachedViews方法对所有的ItemView进行回收,这部分的内容属于RecyclerView缓存机制的部分。

接下来我们来看看第三步,也就是调用fill方法来填充Children。在正式分析填充过程时,我们先来看一张图片:

 

上图形象的展现出三种fill的情况。其中,我们可以看到第三种情况,fill方法被调用了两次。我们看看fill方法:

    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);

        }
         // ······
    }

fill方法的代码比较长,其实都是来计算可填充的空间,真正填充Child的地方是layoutChunk方法。我们来看看layoutChunk方法。
由于layoutChunk方法比较长,这里我就不完整的展示,为了方便理解,我对这个方法做一个简单的概述,让大家有一个大概的理解。

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

至于每一步具体干了嘛,这里就不详细的解释,都是一些基本操作。综上所述,便是LayoutManageronLayoutChildren方法整个执行过程。

6.总结

  1. RecyclerViewmeasure过程分为三种情况,每种情况都有执行过程。通常来说,我们都会走自动测量的过程。
  2. 自动测量里面需要分清楚mState.mLayoutStep状态值,因为根据不同的状态值调用不同的dispatchLayoutStep方法。
  3. layout过程也根据mState.mLayoutStep状态来调用不同的dispatchLayoutStep方法
  4. draw过程主要做了四件事:1.绘制ItemDecorationonDraw部分;2.绘制Children;3.绘制ItemDecorationdrawOver部分;4. 根据mClipToPadding的值来判断是否进行特殊绘制。

二.Recycleview的滑动机制

        从RecyclerView的类结构上来看,我们知道RecyclerView实现了NestedScrollingChild接口,所以RecyclerView也是一个可以产生滑动事件的View。我相信大家都有用过CoordinatorLayoutRecyclerView这个组合,这其中原理的也是嵌套滑动。

本文打算从如下几个方面来分析RecyclerView

  1. 正常的TouchEvent
  2. 嵌套滑动(穿插着文章各个地方,不会专门的讲解)
  3. 多指滑动
  4. fling滑动

1.传统事件

首先我们来看看onTouchEvent方法:

    @Override
    public boolean onTouchEvent(MotionEvent e) {
        // ······
        if (dispatchOnItemTouch(e)) {
            cancelTouch();
            return true;
        }
        // ······
        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                // ······
            } break;

            case MotionEvent.ACTION_POINTER_DOWN: {
                // ······
            } break;

            case MotionEvent.ACTION_MOVE: {
                // ······
            } break;

            case MotionEvent.ACTION_POINTER_UP: {
                // ······
            } break;

            case MotionEvent.ACTION_UP: {
                // ······
            } break;

            case MotionEvent.ACTION_CANCEL: {
                cancelTouch();
            } break;
        }
        // ······
        return true;
    }

ACTION_POINTER_DOWNACTION_POINTER_UP事件比较陌生,这两个事件就跟多指滑动有关,

在分析源码之前,我先将上面的代码做一个简单的概述。

  1. 如果当前的mActiveOnItemTouchListener需要消耗当前事件,那么优先交给它处理。
  2. 如果mActiveOnItemTouchListener不消耗当前事件,那么就走正常的事件分发机制。这里面有很多的细节,稍后我会详细的介绍。

关于第一步,这里不用我来解释,它就是一个Listener的回调,非常的简单,我们重点在于分析第二步。

(1)down事件

            case MotionEvent.ACTION_DOWN: {
                mScrollPointerId = e.getPointerId(0);
                mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
                mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);

                int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
                if (canScrollHorizontally) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
                }
                if (canScrollVertically) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
                }
                startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
            } break;

这里主要做了两件事:

  1. 记录下Down事件的x、y坐标。
  2. 调用startNestedScroll方法,询问父View是否处理事件。

(2)move事件

move事件的源码比较长,且不难读懂,源码就先不放此文了,直接说下move的步骤:

  1. 根据Move事件产生的x、y坐标来计算dx、dy。
  2. 调用dispatchNestedPreScroll询问父View是否优先处理滑动事件,如果要消耗,dx和dy分别会减去父View消耗的那部分距离。
  3. 然后根据情况来判断RecyclerView是垂直滑动还是水平滑动,最终是调用scrollByInternal方法来实现滑动的效果的。
  4. 调用GapWorkerpostFromTraversal来预取ViewHolder。这个过程会走缓存机制部分的逻辑,同时也有可能会调用AdapteronBindViewHolder方法来提前加载数据。

其中第一步和第二步都是比较简单的,这里就直接省略。
  而scrollByInternal方法也是非常的简单,在scrollByInternal方法内部,实际上是调用了LayoutManagerscrollHorizontallyBy方法或者scrollVerticallyBy方法来实现的。LayoutManager这两个方法实际上也没有做什么比较骚的操作,归根结底,最终调用了就是调用了每个ChildoffsetTopAndBottom或者offsetLeftAndRight方法来实现的,这里就不一一的跟踪代码了。

在这里,我们就简单分析下GapWorker是怎么进行领取的,看看postFromTraversal方法:

    void postFromTraversal(RecyclerView recyclerView, int prefetchDx, int prefetchDy) {
        if (recyclerView.isAttachedToWindow()) {
            if (RecyclerView.DEBUG && !mRecyclerViews.contains(recyclerView)) {
                throw new IllegalStateException("attempting to post unregistered view!");
            }
            if (mPostTimeNs == 0) {
                mPostTimeNs = recyclerView.getNanoTime();
                recyclerView.post(this);
            }
        }

        recyclerView.mPrefetchRegistry.setPrefetchVector(prefetchDx, prefetchDy);
    }

postFromTraversal方法内部也没有做多少事情,最核心在于调用了post方法,向任务队列里面添加了一个Runnable。看来重点的分析还是GapWorkerrun方法:

    @Override
    public void run() {
        try {
            TraceCompat.beginSection(RecyclerView.TRACE_PREFETCH_TAG);

            if (mRecyclerViews.isEmpty()) {
                // abort - no work to do
                return;
            }

            // Query most recent vsync so we can predict next one. Note that drawing time not yet
            // valid in animation/input callbacks, so query it here to be safe.
            final int size = mRecyclerViews.size();
            long latestFrameVsyncMs = 0;
            for (int i = 0; i < size; i++) {
                RecyclerView view = mRecyclerViews.get(i);
                if (view.getWindowVisibility() == View.VISIBLE) {
                    latestFrameVsyncMs = Math.max(view.getDrawingTime(), latestFrameVsyncMs);
                }
            }

            if (latestFrameVsyncMs == 0) {
                // abort - either no views visible, or couldn't get last vsync for estimating next
                return;
            }

            long nextFrameNs = TimeUnit.MILLISECONDS.toNanos(latestFrameVsyncMs) + mFrameIntervalNs;

            prefetch(nextFrameNs);

            // TODO: consider rescheduling self, if there's more work to do
        } finally {
            mPostTimeNs = 0;
            TraceCompat.endSection();
        }
    }

run方法的逻辑也是非常简单,首先计算获得下一帧的时间,然后调用prefetch方法进行预取ViewHolder

    void prefetch(long deadlineNs) {
        buildTaskList();
        flushTasksWithDeadline(deadlineNs);
    }

  prefetch方法也简单,显示调用buildTaskList方法生成任务队列,然后调用flushTasksWithDeadline来执行task,这其中会调用RecyclerViewtryGetViewHolderForPositionByDeadline方法来获取一个ViewHolder,这里就不一一分析了。
  不过需要提一句的是,tryGetViewHolderForPositionByDeadline方法是整个RecyclerView缓存机制的核心,RecyclerView缓存机制在这个方法被淋漓尽致的体现出来。最后就是Up事件和Cancel事件,这两个事件更加的简单,都进行一些清理的操作,这里就不分析了。不过在Up事件里面,有一个特殊事件可能会产生–fling事件,待会我们会详细的分析。

2.多指滑动

多指滑动的意思不是指RecyclerView能够相应多根手指的滑动,而是指当一个手指还没释放时,此时另一个手指按下,此时RecyclerView就不相应上一个手指的手势,而是相应最近按下手指的手势。

            case MotionEvent.ACTION_POINTER_DOWN: {
                mScrollPointerId = e.getPointerId(actionIndex);
                mInitialTouchX = mLastTouchX = (int) (e.getX(actionIndex) + 0.5f);
                mInitialTouchY = mLastTouchY = (int) (e.getY(actionIndex) + 0.5f);
            } break;

当另一个手指按下时,此时就会立即更新按下的坐标,同时会更新mScrollPointerId,表示后面只会响应最近按下手指的手势。
其次,我们来看看多指松开的情况:

            case MotionEvent.ACTION_POINTER_UP: {
                onPointerUp(e);
            } break;

    private void onPointerUp(MotionEvent e) {
        final int actionIndex = e.getActionIndex();
        if (e.getPointerId(actionIndex) == mScrollPointerId) {
            // Pick a new pointer to pick up the slack.
            final int newIndex = actionIndex == 0 ? 1 : 0;
            mScrollPointerId = e.getPointerId(newIndex);
            mInitialTouchX = mLastTouchX = (int) (e.getX(newIndex) + 0.5f);
            mInitialTouchY = mLastTouchY = (int) (e.getY(newIndex) + 0.5f);
        }
    }

在这里没有什么特别的操作,就是普通的更新。

3.fling滑动

我们先来看看fling滑动产生的地方,也是Up事件的地方:

          case MotionEvent.ACTION_UP: {
                mVelocityTracker.addMovement(vtev);
                eventAddedToVelocityTracker = true;
                mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
                final float xvel = canScrollHorizontally
                        ? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
                final float yvel = canScrollVertically
                        ? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
                if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
                    setScrollState(SCROLL_STATE_IDLE);
                }
                resetTouch();
            } break;

从上面的代码中,我们可以看出来,最终是调用fling方法来是实现fling效果的,我们来看看fling方法:

    public boolean fling(int velocityX, int velocityY) {
        // ······
        if (!dispatchNestedPreFling(velocityX, velocityY)) {
            final boolean canScroll = canScrollHorizontal || canScrollVertical;
            dispatchNestedFling(velocityX, velocityY, canScroll);

            if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) {
                return true;
            }

            if (canScroll) {
                int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
                if (canScrollHorizontal) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
                }
                if (canScrollVertical) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
                }
                startNestedScroll(nestedScrollAxis, TYPE_NON_TOUCH);

                velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
                velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
                mViewFlinger.fling(velocityX, velocityY);
                return true;
            }
        }
        return false;
    }

fling方法里面,显示调用dispatchNestedPreFling方法询问父View是否处理fling事件,最后调用ViewFlingerfling方法来实现fling效果,所以真正的核心在于ViewFlingerfling方法里面,我们继续来看:

        public void fling(int velocityX, int velocityY) {
            setScrollState(SCROLL_STATE_SETTLING);
            mLastFlingX = mLastFlingY = 0;
            mScroller.fling(0, 0, velocityX, velocityY,
                    Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
            postOnAnimation();
        }

ViewFlingerfling方法里面,先是调用了OverScrollerfling来计算fling相关的参数,包括fling的距离和fling的时间。这里就不深入的分析计算相关的代码,因为这里面都是一些数学和物理的计算。最后就是调用了postOnAnimation方法。

        void postOnAnimation() {
            if (mEatRunOnAnimationRequest) {
                mReSchedulePostAnimationCallback = true;
            } else {
                removeCallbacks(this);
                ViewCompat.postOnAnimation(RecyclerView.this, this);
            }
        }

其实跟Viewpost差不多,所以最终还是得看ViewFlingerrun方法。ViewFlingerrun方法比较长,这里我将它简化了一下:

        public void run() {
            // ······
            // 第一步,更新滚动信息,并且判断当前是否已经滚动完毕
            // 为true表示未滚动完毕
            if (scroller.computeScrollOffset()) {
                //······

                if (mAdapter != null) {
                    // ······
                    // 滚动特定距离
                    if (dx != 0) {
                        hresult = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
                        overscrollX = dx - hresult;
                    }
                    if (dy != 0) {
                        vresult = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
                        overscrollY = dy - vresult;
                    }
                    // ······
                }
                // ······
                // 如果滚动完毕,就是调用finish方法;
                // 如果没有滚动完毕,就调用postOnAnimation方法继续递归
                if (scroller.isFinished() || (!fullyConsumedAny
                        && !hasNestedScrollingParent(TYPE_NON_TOUCH))) {
                    // setting state to idle will stop this.
                    setScrollState(SCROLL_STATE_IDLE);
                    if (ALLOW_THREAD_GAP_WORK) {
                        mPrefetchRegistry.clearPrefetchPositions();
                    }
                    stopNestedScroll(TYPE_NON_TOUCH);
                } else {
                    postOnAnimation();
                    if (mGapWorker != null) {
                        mGapWorker.postFromTraversal(RecyclerView.this, dx, dy);
                    }
                }
            }
            // ······
        }

整个fling核心就在这里,通过上面的三步,最终就是实现了fling的效果,上面的注意已经非常的清晰了,这里就不继续分析了。

三.recycleview的缓存机制

1.概述

(1).四级缓存

首先,我将RecyclerView的缓存分为四级,可能有的人将它分为三级,这些看个人的理解。这里统一说明一下每级缓存的意思。

缓存级别

实际变量

含义

一级缓存mAttachedScrapmChangedScrap

这是优先级最高的缓存,RecyclerView在获取ViewHolder时,优先会到这两个缓存来找。其中mAttachedScrap存储的是当前还在屏幕中的ViewHoldermChangedScrap存储的是数据被更新的ViewHolder,比如说调用了AdapternotifyItemChanged方法。

二级缓存mCachedViews默认大小为2,通常用来存储预取的ViewHolder,同时在回收ViewHolder时,也会可能存储一部分的ViewHolder,这部分的ViewHolder通常来说,意义跟一级缓存差不多。
三级缓存ViewCacheExtension自定义缓存,通常用不到
四级缓存RecyclerViewPool根据ViewType来缓存ViewHolder,每个ViewType的数组大小为5,可以动态的改变。

如上表,统一的解释了每个缓存的含义和作用。在这里,我再来对其中的几个缓存做一个详细的解释。

  1. mAttachedScrap:上表中说,它表示存储的是当前还在屏幕中ViewHolder。实际上是从屏幕上分离出来的ViewHolder,但是又即将添加到屏幕上去的ViewHolder。比如说,RecyclerView上下滑动,滑出一个新的Item,此时会重新调用LayoutManageronLayoutChildren方法,从而会将屏幕上所有的ViewHolderscrap掉(含义就是废弃掉),添加到mAttachedScrap里面去,然后在重新布局每个ItemView时,会从优先mAttachedScrap里面获取,这样效率就会非常的高。这个过程不会重新onBindViewHolder
  2. mCachedViews:默认大小为2,不过通常是3,3由默认的大小2 + 预取的个数1。所以在RecyclerView在首次加载时,mCachedViewssize为3(这里以LinearLayoutManager的垂直布局为例)。通常来说,可以通过RecyclerViewsetItemViewCacheSize方法设置大小,但是这个不包括预取大小;预取大小通过LayoutManagersetItemPrefetchEnabled方法来控制。

(2).ViewHolder的几个状态值

我们在看RecyclerView的源码时,可能到处都能看到调用ViewHolderisInvalidisRemovedisBoundisTmpDetachedisScrapisUpdated这几个方法。

方法名

对应的flag

含义或者状态设置的时机

isInvalidFLAG_INVALID

表示当前ViewHolder是否已经失效。通常来说,在3种情况下会出现这种情况:1.调用了AdapternotifyDataSetChanged方法;2. 手动调用RecyclerViewinvalidateItemDecorations方法;3. 调用RecyclerViewsetAdapter方法或者swapAdapter方法。

isRemovedFLAG_REMOVED表示当前的ViewHolder是否被移除。通常来说,数据源被移除了部分数据,然后调用AdapternotifyItemRemoved方法。
isBoundFLAG_BOUND表示当前ViewHolder是否已经调用了onBindViewHolder
isTmpDetachedFLAG_TMP_DETACHED

表示当前的ItemView是否从RecyclerView(即父View)detach掉。通常来说有两种情况下会出现这种情况:1.手动了RecyclerViewdetachView相关方法;2. 在从mHideViews里面获取ViewHolder,会先detach掉这个ViewHolder关联的ItemView

isScrap无Flag来表示该状态,用mScrapContainer是否为null来判断表示是否在mAttachedScrap或者mChangedScrap数组里面,进而表示当前ViewHolder是否被废弃。
isUpdatedFLAG_UPDATE

表示当前ViewHolder是否已经更新。通常来说,在3种情况下会出现:1.isInvalid方法存在的三种情况;2.调用了AdapteronBindViewHolder方法;3. 调用了AdapternotifyItemChanged方法

(3).ChildHelper的mHiddenViews

在四级缓存中,我们并没有将mHiddenViews算入其中。因为mHiddenViews只在动画期间才会有元素,当动画结束了,自然就清空了。所以mHiddenViews并不算入4级缓存中。
  这里还有一个问题,就是上面在解释mChangedScrap时,也在说,当调用AdapternotifyItemChanged方法,会将更新了的ViewHolder反放入mChangedScrap数组里面。那到底是放入mChangedScrap还是mHiddenViews呢?同时可能有人对mChangedScrapmAttachedScrap有疑问,这里我做一个统一的解释:

首先,如果调用了AdapternotifyItemChanged方法,会重新回调到LayoutManageronLayoutChildren方法里面,而在onLayoutChildren方法里面,会将屏幕上所有的ViewHolder回收到mAttachedScrapmChangedScrap。这个过程就是将ViewHolder分别放到mAttachedScrapmChangedScrap,而什么条件下放在mAttachedScrap,什么条件放在mChangedScrap,这个就是他们俩的区别。

接下来我们来看一段代码,就能分清mAttachedScrapmChangedScrap的区别了:

        void scrapView(View view) {
            final ViewHolder holder = getChildViewHolderInt(view);
            if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
                    || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
                if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {
                    throw new IllegalArgumentException("Called scrap view with an invalid view."
                            + " Invalid views cannot be reused from scrap, they should rebound from"
                            + " recycler pool." + exceptionLabel());
                }
                holder.setScrapContainer(this, false);
                mAttachedScrap.add(holder);
            } else {
                if (mChangedScrap == null) {
                    mChangedScrap = new ArrayList<ViewHolder>();
                }
                holder.setScrapContainer(this, true);
                mChangedScrap.add(holder);
            }
        }

可能很多人初次看到这方法时,会非常的懵逼,我也是如此。今天我们就来看看这个方法。这个根本的目的就是,判断ViewHolder的flag状态,从而来决定是放入mAttachedScrap还是mChangedScrap。从上面的代码,我们得出:

  1. mAttachedScrap里面放的是两种状态的ViewHolder:1.被同时标记为removeinvalid;2.完全没有改变的ViewHolder。这里还有第三个判断,这个跟RecyclerViewItemAnimator有关,如果ItemAnimator为空或者ItemAnimatorcanReuseUpdatedViewHolder方法为true,也会放入到mAttachedScrap。那正常情况下,什么情况返回为true呢?从SimpleItemAnimator的源码可以看出来,当ViewHolderisInvalid方法返回为true时,会放入到 mAttachedScrap里面。也就是说,如果ViewHolder失效了,也会放到mAttachedScrap里面。
  2. 那么mChangedScrap里面放什么类型flag的ViewHolder呢?当然是ViewHolderisUpdated方法返回为true时,会放入到mChangedScrap里面去。所以,调用AdapternotifyItemChanged方法时,并且RecyclerViewItemAnimator不为空,会放入到mChangedScrap里面。

了解了mAttachedScrapmChangedScrap的区别之后,接下我们来看Scrap数组和mHiddenViews的区别。

mHiddenViews只存放动画的ViewHolder,动画结束了自然就清空了。之所以存在 mHiddenViews这个数组,我猜测是存在动画期间,进行复用的可能性,此时就可以在mHiddenViews进行复用了。而Scrap数组跟mHiddenViews两者完全不冲突,所以存在一个ViewHolder同时在Scrap数组和mHiddenViews的可能性。但是这并不影响,因为在动画结束时,会从mHiddenViews里面移除。

2.复用

RecyclerViewViewHolder的复用,我们得从LayoutStatenext方法开始。LayoutManager在布局itemView时,需要获取一个ViewHolder对象,就是通过这个方法来获取,具体的复用逻辑也是在这个方面开始调用的。我们来看看:

        View next(RecyclerView.Recycler recycler) {
            if (mScrapList != null) {
                return nextViewFromScrapList();
            }
            final View view = recycler.getViewForPosition(mCurrentPosition);
            mCurrentPosition += mItemDirection;
            return view;
        }

next方法里面其实也没做什么事,就是调用RecyclerViewgetViewForPosition方法来获取一个View的。而getViewForPosition方法最终会调用到RecyclerViewtryGetViewHolderForPositionByDeadline方法。所以,RecyclerView真正复用的核心就在这个方法,我们今天来详细的分析一下这个方法。

(1).通过position方式来获取ViewHolder

通过这种方式来获取优先级比较高,因为每个ViewHolder还没被改变,通常在这种情况下,都是某一个ItemView对应的ViewHolder被更新导致的,所以在屏幕上其他的ViewHolder,可以快速对应原来的ItemView。我们来看看相关的源码。

            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;
                    }
                }
            }

如上的代码分为两步:

  1. mChangedScrap里面去获取ViewHolder,这里面存储的是更新的ViewHolder
  2. 分别mAttachedScrap、 mHiddenViewsmCachedViews获取ViewHolder。

我们来简单的分析一下这两步。先来看看第一步。

            if (mState.isPreLayout()) {
                holder = getChangedScrapViewForPosition(position);
                fromScrapOrHiddenOrCache = holder != null;
            }

如果当前是预布局阶段,那么就从mChangedScrap里面去获取ViewHolder。那什么阶段是预布局阶段呢?这里我对预布局这个概念简单的解释。

预布局又可以称之为preLayout,当当前的RecyclerView处于dispatchLayoutStep1阶段时,称之为预布局;dispatchLayoutStep2称为真正布局的阶段;dispatchLayoutStep3称为postLayout阶段。同时要想真正开启预布局,必须有ItemAnimator,并且每个RecyclerView对应的LayoutManager必须开启预处理动画。

在这里,为了简单,只要RecyclerView处于dispatchLayoutStep1,我们就当做它处于预布局阶段。

为什么只在预布局的时候才从mChangedScrap里面去取呢?

首先,我们得知道mChangedScrap数组里面放的是什么类型的 ViewHolder。从前面的分析中,我们知道,只有当ItemAnimator不为空,被changed的ViewHolder会放在mChangedScrap数组里面。因为chang动画前后相同位置上的ViewHolder是不同的,所以当预布局时,从mChangedScrap缓存里面取,而正式布局时,不会从mChangedScrap缓存里面去,这就保证了动画前后相同位置上是不同的ViewHolder。为什么要保证动画前后是不同的ViewHolder呢?这是RecyclerView动画机制相关的知识,这里就不详细的解释,后续有专门的文章来分析它,在这里,我们只需要记住,chang动画执行的有一个前提就是动画前后是不同的ViewHolder

然后,我们再来看看第二步。

            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;
                    }
                }
            }

这一步理解起来比较容易,分别从mAttachedScrap、 mHiddenViewsmCachedViews获取ViewHolder。但是我们需要的是,如果获取的ViewHolder是无效的,得做一些清理操作,然后重新放入到缓存里面,具体对应的缓存就是mCacheViewsRecyclerViewPoolrecycleViewHolderInternal方法就是回收ViewHolder的方法,后面再分析回收相关的逻辑会重点分析这个方法,这里就不进行追究了。

(2).通过viewType方式来获取ViewHolder

前面分析了通过Position的方式来获取ViewHolder,这里我们来分析一下第二种方式--ViewType。不过在这里,我先对前面的方式做一个简单的总结,RecyclerView通过Position来获取ViewHolder,并不需要判断ViewType是否合法,因为如果能够通过Position来获取ViewHolderViewType本身就是正确对应的。
而这里通过ViewType来获取ViewHolder表示,此时ViewHolder缓存的Position已经失效了。ViewType方式来获取ViewHolder的过程,我将它分为3步:

  1. 如果AdapterhasStableIds方法返回为true,优先通过ViewTypeid两个条件来寻找。如果没有找到,那么就进行第2步。
  2. 如果AdapterhasStableIds方法返回为false,在这种情况下,首先会在ViewCacheExtension里面找,如果还没有找到的话,最后会在RecyclerViewPool里面来获取ViewHolder。
  3. 如果以上的复用步骤都没有找到合适的ViewHolder,最后就会调用AdapteronCreateViewHolder方法来创建一个新的ViewHolder

在这里,我们需要注意的是,上面的第1步 和 第2步有前提条件,就是两个都必须比较ViewType。接下来,我通过代码简单的分析一下每一步。

A.通过id寻找合适的ViewHolder

通过id寻找合适的ViewHolder主要是通过调用getScrapOrCachedViewForId方法来实现的,我们简单的看一下代码:

                // 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;
                    }
                }

getScrapOrCachedViewForId方法本身没有什么分析的必要,就是分别从mAttachedScrapmCachedViews数组寻找合适的ViewHolder

B.从RecycleViewPool里面获取ViewHolder

ViewCacheExtension存在的情况是非常的少见,这里为了简单,就不展开了(实际上我也不懂!),所以这里,我们直接来看RecyclerViewPool方式。
在这里,我们需要了解RecyclerViewPool的数组结构。我们简单的分析一下RecyclerViewPool这个类。

        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<>();

RecyclerViewPool的内部,使用SparseArray来存储每个ViewType对应的ViewHolder数组,其中每个数组的最大size为5。这个数据结构是不是非常简单呢?
简单的了解了RecyclerViewPool的数据结构,接下来我们来看看复用的相关的代码:

                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);
                        }
                    }
                }

C.调用Adapter的onCreateViewHolder方法创建一个新的ViewHolder

                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");
                    }
                }

上面的代码主要的目的就是调用AdaptercreateViewHolder方法来创建一个ViewHolder,在这个过程就是简单计算了创建一个ViewHolder的时间。

3.回收

回收是RecyclerView复用机制内部非常重要。首先,有复用的过程,肯定就有回收的过程;其次,同时理解了复用和回收两个过程,这可以帮助我们在宏观上理解RecyclerView的工作原理;最后,理解RecyclerView在何时会回收ViewHolder,这对使用RecyclerView有很大的帮助。
其实回收的机制也没有想象中那么的难,本文打算从几个方面来分析RecyclerView的回收过程。

  1. scrap数组
  2. mCacheViews数组
  3. mHiddenViews数组
  4. RecyclerViewPool数组

(1).scrap数组

关于ViewHolder回收到scrap数组里面,其实我在前面已经简单的分析了,重点就在于RecyclerscrapView方法里面。我们来看看scrapView在哪里被调用了。有如下两个地方:

  1. getScrapOrHiddenOrCachedHolderForPosition方法里面,如果从mHiddenViews获得一个ViewHolder的话,会先将这个ViewHoldermHiddenViews数组里面移除,然后调用RecyclerscrapView方法将这个ViewHolder放入到scrap数组里面,并且标记FLAG_RETURNED_FROM_SCRAPFLAG_BOUNCED_FROM_HIDDEN_LIST两个flag。
  2. LayoutManager里面的scrapOrRecycleView方法也会调用RecyclerscrapView方法。而有两种情形下会出现如此情况:1. 手动调用了LayoutManager相关的方法;2. RecyclerView进行了一次布局(调用了requestLayout方法)

(2).mCacheViews数组

mCacheViews数组作为二级缓存,回收的路径相较于一级缓存要多。关于mCacheViews数组,重点在于RecyclerrecycleViewHolderInternal方法里面。我将mCacheViews数组的回收路径大概分为三类,我们来看看:

  1. 在重新布局回收了。这种情况主要出现在调用了AdapternotifyDataSetChange方法,并且此时AdapterhasStableIds方法返回为false。从这里看出来,为什么notifyDataSetChange方法效率为什么那么低,同时也知道了为什么重写hasStableIds方法可以提高效率。因为notifyDataSetChange方法使得RecyclerView将回收的ViewHolder放在二级缓存,效率自然比较低。
  2. 在复用时,从一级缓存里面获取到ViewHolder,但是此时这个ViewHolder已经不符合一级缓存的特点了(比如Position失效了,跟ViewType对不齐),就会从一级缓存里面移除这个ViewHolder,从添加到mCacheViews里面
  3. 当调用removeAnimatingView方法时,如果当前ViewHolder被标记为remove,会调用recycleViewHolderInternal方法来回收对应的ViewHolder。调用removeAnimatingView方法的时机表示当前的ItemAnimator已经做完了

(3).mHiddenViews数组

一个ViewHolder回收到mHiddenView数组里面的条件比较简单,如果当前操作支持动画,就会调用到RecyclerViewaddAnimatingView方法,在这个方法里面会将做动画的那个View添加到mHiddenView数组里面去。通常就是动画期间可以会进行复用,因为mHiddenViews只在动画期间才会有元素。

(4).RecyclerViewPool

RecyclerViewPoolmCacheViews,都是通过recycleViewHolderInternal方法来进行回收,所以情景与mCacheViews差不多,只不过当不满足放入mCacheViews时,才会放入到RecyclerViewPool里面去。

(5).为什么hasStableIds方法返回true会提高效率呢?

了解了RecyclerView的复用和回收机制之后,这个问题就变得很简单了。我从两个方面来解释原因。

A.复用方面

我们先来看看复用怎么能体现hasStableIds能提高效率呢?来看看代码:

                if (mAdapter.hasStableIds()) {
                    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                            type, dryRun);
                    if (holder != null) {
                        // update position
                        holder.mPosition = offsetPosition;
                        fromScrapOrHiddenOrCache = true;
                    }
                }

在前面通过Position方式来获取一个ViewHolder失败之后,如果AdapterhasStableIds方法返回为true,在进行通过ViewType方式来获取ViewHolder时,会优先到1级或者二级缓存里面去寻找,而不是直接去RecyclerViewPool里面去寻找。从这里,我们可以看到,在复用方面,hasStableIds方法提高了效率。

B.回收方面

        private void scrapOrRecycleView(Recycler recycler, int index, View view) {
            final ViewHolder viewHolder = getChildViewHolderInt(view);
            if (viewHolder.shouldIgnore()) {
                if (DEBUG) {
                    Log.d(TAG, "ignoring view " + viewHolder);
                }
                return;
            }
            if (viewHolder.isInvalid() && !viewHolder.isRemoved()
                    && !mRecyclerView.mAdapter.hasStableIds()) {
                removeViewAt(index);
                recycler.recycleViewHolderInternal(viewHolder);
            } else {
                detachViewAt(index);
                recycler.scrapView(view);
                mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
            }
        }

从上面的代码中,我们可以看出,如果hasStableIds方法返回为true的话,这里所有的回收都进入scrap数组里面。这刚好与前面对应了。
通过如上两点,我们就能很好的理解为什么hasStableIds方法返回true会提高效率。

4.总结

RecyclerView回收和复用机制到这里分析的差不多了。这里做一个小小的总结。

  1. RecyclerView内部有4级缓存,每一级的缓存所代表的意思都不一样,同时复用的优先也是从上到下,各自的回收也是不一样。
  2. mHideenViews的存在是为了解决在动画期间进行复用的问题。
  3. ViewHolder内部有很多的flag,在理解回收和复用机制之前,最好是将ViewHolder的flag梳理清楚。

最后用一张图片来结束本章节的介绍。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值