RecyclerView 源码分析

RecyclerView 源码分析

本文原创,转载请注明出处。
欢迎关注我的 简书 ,关注我的专题 Android Class 我会长期坚持为大家收录简书上高质量的 Android 相关博文。

写在前面:
RecyclerView 是一个越用越优雅的控件,相信大家对于 RecyclerView 的使用也已经比较熟悉了。其功能的高度解耦化,规范 ViewHolder 的写法,以及对动画友好支持,都是它与传统控件 ListView 的区别。而无论 ListView 还是 RecyclerView,本质上都是在有限的屏幕之上,展示大量的内容。所以复用的逻辑,就成了它们最最重要的核心原理,本文主要目的就是探究 RecyclerView 的复用原理。

RecyclerView 的几大模块

  • LayoutManager
    负责 RecyclerView 中,控制 item 的布局方向

  • RecyclerView.Adapter
    为 RecyclerView 承载数据

  • ItemDecoration
    为 RecyclerView 添加分割线

  • ItemAnimator
    控制 RecyclerView 中 item 的动画

刚刚我们提到 RecyclerView 的高度解耦,就是通过以上对象各司其职,来实现 RecyclerView 的基本功能。RecyclerView 无论多么复杂,本质上也是一个自定义 View,本文的重点就是缓存原理分析,不过在此之前,我们还是简单地分别介绍下以上中各个模块的源码。

ItemDecoration

    public void addItemDecoration(ItemDecoration decor, int index) {
        if (mLayout != null) {
            mLayout.assertNotInLayoutOrScroll("Cannot add item decoration during a scroll  or"
                    + " layout");
        }
        if (mItemDecorations.isEmpty()) {
            setWillNotDraw(false);
        }
        if (index < 0) {
            mItemDecorations.add(decor);
        } else {
            // 指定添加分割线在集合中的索引
            mItemDecorations.add(index, decor);
        }
        markItemDecorInsetsDirty();
        // 重新请求 View 的测量、布局、绘制
        requestLayout();
    }

mItemDecorations 是一个 ArrayList,我们将 ItemDecoration 也就是分割线对象,添加到其中。接着我们看下 markItemDecorInsetsDirty 这个方法做了些什么。

    void markItemDecorInsetsDirty() {
        final int childCount = mChildHelper.getUnfilteredChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = mChildHelper.getUnfilteredChildAt(i);
            ((LayoutParams) child.getLayoutParams()).mInsetsDirty = true;
        }
        mRecycler.markItemDecorInsetsDirty();
    }

这个方法首先遍历了 RecyclerView 和 LayoutManager 的所有子 View,将其子 View 的 LayoutParams 中的 mInsetsDirty 属性置为 true。接着调用了 mRecycler.markItemDecorInsetsDirty(),Recycler 是 RecyclerView 的一个内部类,就是它管理着 RecyclerView 的复用逻辑。这个我们一会再细谈。

        void markItemDecorInsetsDirty() {
            final int cachedCount = mCachedViews.size();
            for (int i = 0; i < cachedCount; i++) {
                final ViewHolder holder = mCachedViews.get(i);
                LayoutParams layoutParams = (LayoutParams) holder.itemView.getLayoutParams();
                if (layoutParams != null) {
                    layoutParams.mInsetsDirty = true;
                }
            }
        }

mCachedViews 见名知意,也就是 RecyclerView 缓存的集合,相信你也看到了,RecyclerView 的缓存单位是 ViewHolder。我们在 ViewHolder 中取出 itemView,然后获得 LayoutParams,将其 mInsetsDirty 字段一样置为 true。

mInsetsDirty 字段的作用其实是一种优化性能的缓存策略,添加分割线对象时,无论是 RecyclerView 的子 view,还是缓存的 view,都将其置为 true,接着就调用了 requestLayout 方法。

这里简单说一下 requestLayout 方法用一种责任链的方式,层层向上传递,最后传递到 ViewRootImpl,然后重新调用 view 的 measure、layout、draw 方法来展示布局。

我们在 RecyclerView 中搜索 mItemDecorations 集合,看看他是在什么时刻操作 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);
        }
    }

draw 方法中:

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

可以看到在 View 的以上两个方法中,分别调用了 ItemDecoration 对象的 onDraw onDrawOver 方法。

这两个抽象方法,由我们继承 ItemDecoration 来自己实现,他们区别就是 onDraw 在 item view 绘制之前调用,onDrawOver 在 item view 绘制之后调用。

所以绘制顺序就是 Decoration 的 onDraw,ItemView的 onDraw,Decoration 的 onDrawOver。

(好像越写越多...收不住了...)

我们在 onDrawonDrawOver 方法中就可以绘制 drawable 对象了。此时分割线就展现出来了。还记得刚才的 mInsetsDirty 字段吗?在添加分割线的时候,无论是 RecyclerView 子 View,还是缓存中的 View,其 LayoutParams 中的 mInsetsDirty 属性,都被置为 true。 我们来解释一下这个字段的作用:

    Rect getItemDecorInsetsForChild(View child) {
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        if (!lp.mInsetsDirty) {
            // 当 mInsetsDirty 为 false,说明 mDecorInsets 缓存可用
            return lp.mDecorInsets;
        }

        if (mState.isPreLayout() && (lp.isItemChanged() || lp.isViewInvalid())) {
            // changed/invalid items should not be updated until they are rebound.
            return lp.mDecorInsets;
        }
        final Rect insets = lp.mDecorInsets;
        insets.set(0, 0, 0, 0);
        final int decorCount = mItemDecorations.size();
        for (int i = 0; i < decorCount; i++) {
            mTempRect.set(0, 0, 0, 0);
            mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
            insets.left += mTempRect.left;
            insets.top += mTempRect.top;
            insets.right += mTempRect.right;
            insets.bottom += mTempRect.bottom;
        }
        lp.mInsetsDirty = false;
        return insets;
    }

来解释一下这段代码,首先 getItemDecorInsetsForChild 方法是在 RecyclerView 进行 measureChild 时调用的。目的就是为了取出 RecyclerView 的 ChildView 中的分割线属性 --- 在 LayoutParams 中缓存的 mDecorInsets 。而 mDecorInsets 就是 Rect 对象, 其保存记录的是所有添加分割线需要的空间累加的总和,由分割线的 getItemOffsets 方法影响。

最后在 measureChild 方法里,将分割线 ItemDecoration 的尺寸加入到 itemView 的 padding 中。

但是大家都知道缓存并不是总是可用的,mInsetsDirty 这个 boolean 字段来记录它的时效性,当 mInsetsDirty 为 false 时,说明缓存可用,直接取出可以,当 mInsetsDirty 为 true 时,说明缓存的分割线属性就需要重新计算了。

到此,关于 RecyclerView 添加分割线 ItemDecoration 的源码分析,也就基本结束了。

ItemAnimator

如果有人问我,在什么情况下你绝对会选择 RecyclerView,而不是 ListView?如果需求对 Item 的动画有一定要求,这绝对是我选择 RecyclerView 的重要原因之一。ListView 如果要做 Item 的增删动画,那可要费很大劲儿,而 RecyclerView 自身对动画就有很好的支持。

    public void setItemAnimator(ItemAnimator animator) {
        if (mItemAnimator != null) {
            mItemAnimator.endAnimations();
            mItemAnimator.setListener(null);
        }
        mItemAnimator = animator;
        if (mItemAnimator != null) {
            mItemAnimator.setListener(mItemAnimatorListener);
        }
    }

这个方法就是为 RecyclerView 设置动画的入口,逻辑就是清除旧的 Listener,设置新的 Listener。这个没什么可多说的,我们这次直接来看看 ItemAnimator 这个类。

当我第一次粗略的看 RecyclerView 的源码之时,感觉这也太多了吧,一万两千多行,这得多复杂啊。后来再看看,才知道,RecyclerView 是把

ItemAnimator 、LayoutManager 等等这些模块都当内部类写在了一块。。。我不知道这样设计的目的是啥,除了写着方便之外可能找不到其他理由了,so... 可能是 google 的程序员偷个懒吧 - -

来看 ItemAnimator 类:

        public @NonNull ItemHolderInfo recordPreLayoutInformation(@NonNull State state,
                @NonNull ViewHolder viewHolder, @AdapterChanges int changeFlags,
                @NonNull List<Object> payloads) {
            return obtainHolderInfo().setFrom(viewHolder);
        }
        public @NonNull ItemHolderInfo recordPostLayoutInformation(@NonNull State state,
                @NonNull ViewHolder viewHolder) {
            return obtainHolderInfo().setFrom(viewHolder);
        }

这两个函数看命名也能猜一个大概,其目的就是为了记录在 RecyclerView 布局之前/之后,必要的一些 layout 信息保存在 ItemHolderInfo 中,ItemHolderInfo 这个类就是用来记录当前 ItemView 的位置信息

recordPreLayoutInformation 来记录 layout 之前的状态信息,这个方法在 dispatchLayoutStep1 之中调用。

dispatchLayoutStep1这个方法做了什么呢,来看看注释就一目了然:

    /**
     * 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
     */

这是布局的第一步:
进行 adapter 布局的更新,决定执行哪个动画,保存当前 view 的信息,如果有必要,运行 predictive layout。

相同的 recordPostLayoutInformation 方法来记录 layout 过程完成时,ItemView 的信息。

它在 dispatchLayoutStep3 方法中调用,dispatchLayoutStep3 方法作用:

    /**
     * The final step of the layout where we save the information about views for animations,
     * trigger animations and do any necessary cleanup.
     */

layout 的最后一个步骤,保存 view 动画的信息,执行动画,状态清理。当然也有 dispatchLayoutStep2 方法,他们三个方法依次在 onLayout 方法的 dispatchLayout方法调用。

dispatchLayout 方法目的是 layout RecyclerView 的 childview,并且记录动画执行的过程、变更。

现在知道了这两个 API 的作用就是在 layout 前后记录 itemview 动画的状态,保存在 ItemHolderInfo 中,我们继续寻找执行动画的 API。

  • animateDisappearance
    当 ViewHolder 从 RecyclerView 的 layout 中移除时,调用
  • animateAppearance
    当 ViewHolder 添加进 RecyclerView 时,调用
  • animatePersistence
    当 ViewHolder 已经添加进 layout 还未移除时,调用
  • animateChange
    当 ViewHolder 已经添加进 layout 还未移除,并且调用了 notifyDataSetChanged 时,调用。

以上四个 api 就是为 RecyclerView 执行动画的。

调用的时机和方式是如何的呢?

    /**
     * The callback to convert view info diffs into animations.
     */
    private final ViewInfoStore.ProcessCallback mViewInfoProcessCallback =
            new ViewInfoStore.ProcessCallback() {
                @Override
                public void processDisappeared(ViewHolder viewHolder, @NonNull ItemHolderInfo info,
                        @Nullable ItemHolderInfo postInfo) {
                    mRecycler.unscrapView(viewHolder);
                    animateDisappearance(viewHolder, info, postInfo);
                }
                @Override
                public void processAppeared(ViewHolder viewHolder,
                        ItemHolderInfo preInfo, ItemHolderInfo info) {
                    animateAppearance(viewHolder, preInfo, info);
                }

                @Override
                public void processPersistent(ViewHolder viewHolder,
                        @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
                    viewHolder.setIsRecyclable(false);
                    if (mDataSetHasChangedAfterLayout) {
                        // since it was rebound, use change instead as we'll be mapping them from
                        // stable ids. If stable ids were false, we would not be running any
                        // animations
                        if (mItemAnimator.animateChange(viewHolder, viewHolder, preInfo,
                                postInfo)) {
                            postAnimationRunner();
                        }
                    } else if (mItemAnimator.animatePersistence(viewHolder, preInfo, postInfo)) {
                        postAnimationRunner();
                    }
                }
                @Override
                public void unused(ViewHolder viewHolder) {
                    mLayout.removeAndRecycleView(viewHolder.itemView, mRecycler);
                }
            };

mViewInfoProcessCallback 是一个匿名内部类,在其回调方法中,分别执行了以上四个关于动画的 api。

继续跟进,看看mViewInfoProcessCallback 这个接口是什么时候被调用执行的:

 // Step 4: Process view info lists and trigger animations
            mViewInfoStore.process(mViewInfoProcessCallback);

执行触发方法的位置就在我们刚才提到的 dispatchLayoutStep3方法中,去根据一些保存的 flag 状态去触发动画。

LayoutManager

与其他绑定 adapter 展示数据的控件,比如 ListView、GrideView 相比,RecyclerView 允许自定义规则去放置子 view,这个规则的控制者就是 LayoutManager。一个 RecyclerView 如果想展示内容,就必须设置一个 LayoutManager。

我们按照惯例来看设置 LayoutManager 的入口:

    public void setLayoutManager(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.
        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;
        if (layout != null) {
            if (layout.mRecyclerView != null) {
                throw new IllegalArgumentException("LayoutManager " + layout +
                        " is already attached to a RecyclerView: " + layout.mRecyclerView);
            }
            mLayout.setRecyclerView(this);
            if (mIsAttached) {
                mLayout.dispatchAttachedToWindow(this);
            }
        }
        mRecycler.updateViewCacheSize();
        requestLayout();
    }

这段代码主要做了一下几件事:
当之前设置过 LayoutManager 时,移除之前的视图,并缓存视图在 Recycler 中,将新的 mLayout 对象与 RecyclerView 绑定,更新缓存 View 的数量。最后去调用 requestLayout ,重新请求 measure、layout、draw。

关于 RecyclerView 的缓存我们一会再研究,先看看设置的 LayoutManager 是在何时何处发挥作用的吧:

LayoutManager 作为 RecyclerView 的一个抽象内部类,大概有3000行代码,方法数还很多,看着头都大了。用有限的时间去了解每一个方法的作用显然不现实。LayoutManager 的作用就是为 RecyclerView 放置子 view,所以我直接去定位 RecyclerView 的 onLayout 和 onMeasure 方法,研究一下 LayoutManager 的一些关键函数的作用。

    @Override
    protected void onMeasure(int widthSpec, int heightSpec) {
        if (mLayout == null) {
            defaultOnMeasure(widthSpec, heightSpec);
            return;
        }
        if (mLayout.mAutoMeasure) {
            final int widthMode = MeasureSpec.getMode(widthSpec);
            final int heightMode = MeasureSpec.getMode(heightSpec);
            final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
                    && heightMode == MeasureSpec.EXACTLY;
            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
            if (skipMeasure || 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);
            }
        } else {
            // 省略非自动测量的情况
        }
    }

来分析一下 onMeasure 方法,mAutoMeasure 字段用来标记是否使用 RecyclerView 的默认规则进行自动测量,否则就必须在 LayoutManager 中自己实现 onMeasure 来进行测量。LinearLayoutManager 的 mAutoMeasure 字段属性就被设置成为了 true。

所以我们重点来看mAutoMeasure为 true 时,测量的规则。

当 RecyclerView 的 MeasureSpecMeasureSpec.EXACTLY时,这个时候可以直接确定 RecyclerView 的宽高,所以 return 退出测量。当 RecyclerView 的宽高为不为 EXACTLY 时,首先进行的测量步骤就是 dispatchLayoutStep1,这个我们在分析动画源码的时候提到过。dispatchLayoutStep1 的作用总结起来就是记录 layout 之前,view 的信息。

接着继续调用了 dispatchLayoutStep2方法:

    /**
     * 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() {
        ...
        // Step 2: Run layout
        ...
        mLayout.onLayoutChildren(mRecycler, mState);
        ...
        mState.mLayoutStep = State.STEP_ANIMATIONS;
    }

dispatchLayoutStep2 比较关键的就是以上代码展示的这两步,onLayoutChildren 这个函数由 LayoutManager 实现,来规定放置子 view 的算法,寻找锚点填充 view,锚点的寻找和填充 view 的方式,这里就不细说了。因为篇幅实在是太长。具体可以直接去看 LinearLayoutManager 的实现方式。

第二步就是将 mState.mLayoutStep 置为 State.STEP_ANIMATIONS,刚才我们忘记说 mLayoutStep 这个属性了,从它的命名就知道它是来标记 layout 这个过程进行到哪一步了。在 dispatchLayoutStep1 中 mState.mLayoutStep 被置为 State.STEP_LAYOUT,记录 layout 的步骤是什么原因呢?来看看 RecyclerView 的 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() {
        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();
    }

看到 mState.mLayoutStep 的作用了吧,当我们在 onMeasure 方法中已经调用过 dispatchLayoutStep1dispatchLayoutStep2 时,在 onLayout 方法中只会调用 dispatchLayoutStep3dispatchLayoutStep3方法我们在动画部分的源码讲解过。

回想一下,什么情况下会在 onMeasure 方法中直接调用 dispatchLayoutStep1dispatchLayoutStep2 ? 就是 RecyclerView 的 MeasureSpec 不为 EXACTLY 时,这个情况下 RecyclerView 不能自己确定自身的宽高,只能在测量、布局了子 view 才能确定自己的宽高。所以在 onMeasure 的时候就调用了 dispatchLayoutStep1dispatchLayoutStep2 ,在 onLayout 仅仅调用 dispatchLayoutStep3 方法就可以了。

如果文字表述的不够清晰,这里来一张图:

onMeasure/onLayout

最后在这个章节中,总结一下 LayoutManager 的作用:

  • 协助 RecyclerView 完成 onMeasure 过程
  • 通过 onLayoutChildren 完成对子 view 的布局
  • 滚动子视图
  • 滚动过程中判断何时添加 view ,何时回收 view,也就是对缓存时机的判断。

Recycler

这篇文章我在工作之余写写停停大概花了一周,终于到了本文的最后一个模块 -- recycler
在上文中反复提到,Recycler 就是控制 RecyclerView 缓存的核心类。理解了它的工作过程,就可以弄明白 RecyclerView 如何回收 view,如何复用 view 的。

在上一个章节,我们说过了 LayoutManager 其实就是 Recycler 的控制者,由 LayoutManager 来决定调用 Recycler 关键方法的时机,口说无凭,就直接来看看 LinearLayoutManager 的代码吧:

首先来看最关键的部分 onLayoutChildren 方法,这个方法是在 RecyclerView 的 dispatchLayoutStep2 阶段调用的。

onLayoutChildren 方法比较长,它核心的作用就是寻找一个锚点,为 RecyclerView 填充子 View。在 onLayoutChildren 调用的 fill 方法就是真正开始填充 layout 的方法。

    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
        // max offset we should set is mFastScroll + available
        final int start = layoutState.mAvailable;
        int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
        LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            layoutChunkResult.resetInternal();

            layoutChunk(recycler, state, layoutState, layoutChunkResult);

            if (layoutChunkResult.mFinished) {
                break;
            }

            if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
                layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
                if (layoutState.mAvailable < 0) {
                    layoutState.mScrollingOffset += layoutState.mAvailable;
                }
                recycleByLayoutState(recycler, layoutState);
            }
        }
        return start - layoutState.mAvailable;
    }

这就是省略了部分代码的 fill 方法,其中的 while 循环中,通过判断 LaytouState 中保存的状态来不断的通过 LayoutChunk 方法填充 view。所以接着再看 LayoutChunk 方法。

    void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
        View view = layoutState.next(recycler);
        if (view == null) {
            // if we are laying out views in scrap, this may return null which means there is
            // no more items to layout.
            result.mFinished = true;
            return;
        }
        LayoutParams params = (LayoutParams) view.getLayoutParams();
        if (layoutState.mScrapList == null) {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addView(view);
            } else {
                addView(view, 0);
            }
        } else {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addDisappearingView(view);
            } else {
                addDisappearingView(view, 0);
            }
        }
    }

以上就是 layoutChunk 的部分代码,可以看到通过 next 方法取出来 view ,并且通过 addView 添加到 RecyclerView 里面去,继续跟进next 方法,看看它是用什么方式和规则取出来 View 的。

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

一路看到这里,我们的 Recycler 终于出现了...跟进 getViewForPosition 方法吧。

在此之前可以先看看 Recycler 几个重要的成员变量,便于我们更好的认识 RecyclerView 的缓存结构:

        final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
        ArrayList<ViewHolder> mChangedScrap = null;

        final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();

        private final List<ViewHolder>
                mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);

        RecycledViewPool mRecyclerPool;

        private ViewCacheExtension mViewCacheExtension;

提前说一下,RecyclerView 其实可以算作是四级缓存、mAttachedScrap 、mCachedViews 、mViewCacheExtension、mRecyclerPool 这四个对象就是作为每一级缓存的结构的。

getViewForPosition 方法:

        View getViewForPosition(int position, boolean dryRun) {
            return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
        }

跟进 tryGetViewHolderForPositionByDeadline方法,一段一段的阅读这个方法代码。
在文章之初我们就说过,RecyclerView 的缓存单元是 ViewHolder,这个tryGetViewHolderForPositionByDeadline 方法就完整的展示了如何在每个层级的缓存中,取出来 ViewHolder,下面我们一步步的分析一下:

            // 0) If there is a changed scrap, try to find from there
            if (mState.isPreLayout()) {
                holder = getChangedScrapViewForPosition(position);
                fromScrapOrHiddenOrCache = holder != null;
            }

第一步,从 mChangedScrap 中尝试取出缓存的 ViewHolder ,若不存在,返回空。

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

如果在第一步发现没有缓存的 ViewHolder,则去 mAttachedScrap 中取,当然 mAttachedScrap 中没有怎么办呢,接着去 mHiddenViews 里面去找,如果还没有,继续从 mCachedViews 中取缓存的 ViewHolder ,这一系列操作是缓存层级的第二步。

            if (holder == null) {
                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
                    throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
                            + "position " + position + "(offset:" + offsetPosition + ")."
                            + "state:" + mState.getItemCount());
                }

                final int type = mAdapter.getItemViewType(offsetPosition);
                // 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;
                    }
                }
                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");
                        } else if (holder.shouldIgnore()) {
                            throw new IllegalArgumentException("getViewForPositionAndType returned"
                                    + " a view that is ignored. You must call stopIgnoring before"
                                    + " returning this view.");
                        }
                    }
                }
                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);
                        }
                    }
                }
                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");
                    }
                }
            }

getScrapOrCachedViewForId 方法中,根据 id 依次在 mAttachedScrap 、mCachedViews 集合中寻找缓存的 ViewHolder,如果都不存在,则在 ViewCacheExtension 对象中寻找缓存,ViewCacheExtension 这个类需要使用者通过 setViewCacheExtension 方法传入,RecyclerView 自身并不会实现它,一般正常的使用也用不到。缓存层级的第三步,我们就分析完毕了,来看最后一步。

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

这段代码是从 RecycledViewPool 中取根据 type 取 ViewHolder,对于 RecycledViewPool 下面多说几句:

        static class ScrapData {
            ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
            int mMaxScrap = DEFAULT_MAX_SCRAP;
            long mCreateRunningAverageNs = 0;
            long mBindRunningAverageNs = 0;
        }
        SparseArray<ScrapData> mScrap = new SparseArray<>();

从这里我们知道,RecycledViewPool 其实是一个 SparseArray 保存 ScrapData 对象的结构。根据 type 缓存 ViewHolder,每个 type,默认最多保存5个 ViewHolder。上面提到的 mCachedViews 这个集合默认最大值是 2 。

RecycledViewPool 可以由多个 ReyclerView 共用。

RecycledViewPool 就是缓存结构中的第四级缓存了,如果 RecycledViewPool 中依然没有缓存的 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");
                    }
                }

这个时候没有办法,就只能调用 mAdapter.createViewHolder(RecyclerView.this, type),来创建一个 ViewHolder 了。

到此我们就知道一个 ViewHolder 是如何层层从缓存中取出的了。

写在后面:
本文从源码的角度研究了 RecyclerView 的主要模块和功能,但是 RecyclerView 本身是很复杂的,要考虑到非常多的情况,光是各种状态的记录就让人看得很迷,这种复杂度的控件真不是一般程序员可以搞定的。不过我们不必深究每一块细节,将大致的流程梳理在心,也肯定会有收获和启发。未来打算总结下 RecyclerView 可能发生的一些复用问题。有疑问可以在下方交流。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容

  • 阅读本文你大概需要10分钟 扯淡的导语好像自从RecyclerView这个控件一出现,对这个控件使用方法的文章就层...
    柴泽建_Jack阅读 23,148评论 26 185
  • 看了又看,任然对其一知半解。用了又用,发现其真的太美。RecyclerView的设计和书写实在是太惊艳了,日常又使...
    deep_sadness阅读 736评论 0 0
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,522评论 25 707
  • 很早就知道萧红,却是在近期才开始想要完整地读她的作品。也是忽然地想起她,忽然的有了想要了解她的冲动。 ...
    简单暖儿阅读 4,442评论 0 0
  • 被爱的人也许不知道,他的一句晚安,可能媲美满天星光。 01, “你最近怎么老是这么晚睡,不是已经到11点了吗?” ...
    龟小慢阅读 5,873评论 5 4