RecyclerView源码

前置条件

你已经理解了View绘制的主要流程。下面主要讲一个RecyclerView的设计之美

什么是RecyclerView

A flexible view for providing a limited window into a large data set
怎么翻译:直译的话有点拗口:我把它翻译成 一个在有限的窗口中表示大量数据集灵活的View 对它就是一个View一个不普通的View。

onMeasure

如果LayoutManger是null ,defaultOnMeasure很简单,可以自行去看,但要知道的是界面上不会显示任何子View因为return了

 if (mLayout == null) {
            defaultOnMeasure(widthSpec, heightSpec);
            return;
        }

自动测量是否开启,RecyclerView自带的几个LayoutManger 重写了 isAutoMeasureEnabled返回的都是true

if (mLayout.isAutoMeasureEnabled()) {
            final int widthMode = MeasureSpec.getMode(widthSpec);
            final int heightMode = MeasureSpec.getMode(heightSpec);

            /**
             * This specific call should be considered deprecated and replaced with
             * {@link #defaultOnMeasure(int, int)}. It can't actually be replaced as it could
             * break existing third party code but all documentation directs developers to not
             * override {@link LayoutManager#onMeasure(int, int)} when
             * {@link LayoutManager#isAutoMeasureEnabled()} returns true.
             */
            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);

            // Calculate and track whether we should skip measurement here because the MeasureSpec
            // modes in both dimensions are EXACTLY.
            mLastAutoMeasureSkippedDueToExact =
                    widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
            if (mLastAutoMeasureSkippedDueToExact || 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);
            }

            mLastAutoMeasureNonExactMeasuredWidth = getMeasuredWidth();
            mLastAutoMeasureNonExactMeasuredHeight = getMeasuredHeight();
        }

和layout=null的情况一样调用的是 defaultOnMeasure(widthSpec, heightSpec)没什么讲的

mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);

设计之美1,有关性能,如果RecyclerView的测量mode是 EXACTLY,其它高度已经定了,直接return,下面的step这些逻辑不用作用了,要知道step1在第一个layout完成后,处理处理adapter设计更改标记时会preLayout(比较耗时的)

 mLastAutoMeasureSkippedDueToExact =
                    widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
            if (mLastAutoMeasureSkippedDueToExact || mAdapter == null) {
                return;
            }

如果走到了下面的代码 dispatchLayoutStep1()被称为预布局

if (mState.mLayoutStep == State.STEP_START) {
                dispatchLayoutStep1();
            }

dispatchLayoutStep1

捕获所有子视图的“旧状态”也就是 “预测位置”为 ItemAnimator(元素动画器)提供数据源供step3使用
省略了部分代码

private void dispatchLayoutStep1() {
processAdapterUpdatesAndSetAnimationFlags();
 if (mState.mRunSimpleAnimations) {
            // Step 0: Find out where all non-removed items are, pre-layout
            int count = mChildHelper.getChildCount();
            for (int i = 0; i < count; ++i) {
                final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
                if (holder.shouldIgnore() || (holder.isInvalid() && !mAdapter.hasStableIds())) {
                    continue;
                }
                final ItemHolderInfo animationInfo = mItemAnimator
                        .recordPreLayoutInformation(mState, holder,
                                ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
                                holder.getUnmodifiedPayloads());
                mViewInfoStore.addToPreLayout(holder, animationInfo);
                if (mState.mTrackOldChangeHolders && holder.isUpdated() && !holder.isRemoved()
                        && !holder.shouldIgnore() && !holder.isInvalid()) {
                    long key = getChangedHolderKey(holder);
                    // This is NOT the only place where a ViewHolder is added to old change holders
                    // list. There is another case where:
                    //    * A VH is currently hidden but not deleted
                    //    * The hidden item is changed in the adapter
                    //    * Layout manager decides to layout the item in the pre-Layout pass (step1)
                    // When this case is detected, RV will un-hide that view and add to the old
                    // change holders list.
                    mViewInfoStore.addToOldChangeHolders(key, holder);
                }
            }
        }
        if (mState.mRunPredictiveAnimations) {
            // Step 1: run prelayout: This will use the old positions of items. The layout manager
            // is expected to layout everything, even removed items (though not to add removed
            // items back to the container). This gives the pre-layout position of APPEARING views
            // which come into existence as part of the real layout.

            // Save old positions so that LayoutManager can run its mapping logic.
            saveOldPositions();
            final boolean didStructureChange = mState.mStructureChanged;
            mState.mStructureChanged = false;
            // temporarily disable flag because we are asking for previous layout
            mLayout.onLayoutChildren(mRecycler, mState);
            mState.mStructureChanged = didStructureChange;

            for (int i = 0; i < mChildHelper.getChildCount(); ++i) {
                final View child = mChildHelper.getChildAt(i);
                final ViewHolder viewHolder = getChildViewHolderInt(child);
                if (viewHolder.shouldIgnore()) {
                    continue;
                }
                if (!mViewInfoStore.isInPreLayout(viewHolder)) {
                    int flags = ItemAnimator.buildAdapterChangeFlagsForAnimations(viewHolder);
                    boolean wasHidden = viewHolder
                            .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
                    if (!wasHidden) {
                        flags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
                    }
                    final ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation(
                            mState, viewHolder, flags, viewHolder.getUnmodifiedPayloads());
                    if (wasHidden) {
                        recordAnimationInfoIfBouncedHiddenView(viewHolder, animationInfo);
                    } else {
                        mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);
                    }
                }
            }
            // we don't process disappearing list because they may re-appear in post layout pass.
            clearOldPositions();
        } else {
            clearOldPositions();
        }
        stopInterceptRequestLayout(false);
        mState.mLayoutStep = State.STEP_LAYOUT;
}

processAdapterUpdatesAndSetAnimationFlags
作用是:处理Adapter更新并设置动画标记,给 mRunSimpleAnimations,mRunPredictiveAnimations(运行预测动画)赋值,这两个值都和 mFirstLayoutComplete有关系,但 mFirstLayoutComplete是在第一次layout后才赋值有true的,所以首次Layout时这两个变量全部为false。首次时Adapter更新个毛啊,初使数据有啥更新的啊。

if (mState.mRunSimpleAnimations) 遍历当前屏幕上所有存在的子视图,记录它们在当前布局下的位置、边界、透明度等信息(即“动画开始前的状态”),并存入 ViewInfoStore, 特别处理 isUpdated()(内容更新) 的 ViewHolder:通过 addToOldChangeHolders 将其与一个唯一 Key(通常是 StableId 或 Position)绑定。这样在后续布局中,即使 View 的内容变了(新旧 View 实例不同),RecyclerView 也能通过 Key 将旧 View 与新 View 配对,从而实现内容变化的交叉淡入淡出动画(旧内容淡出,新内容淡入),一句话总结:给所有“即将消失或移动”的旧视图拍一张“遗照”,作为动画的起始帧
if (mState.mRunPredictiveAnimations) 核心作用:在正式布局前额外执行一次预测性布局(调用 mLayout.onLayoutChildren)。它会让 LayoutManager 基于更新后的 Adapter 数据重新摆放所有子项,但保留已移除项的占位空间。
dispatchLayoutStep1 作用:执行预布局,以时间(机器 大概几十ms)换取 人类感知时间,使用用户感官上页面是流畅的,不会那么突兀,保证动画的视觉连续性

为什么要“预布局”?

1.一个经典的删除场景
假设你有一个竖排列表:[A, B, C, D],现在你删除了 B。
没有预布局(直接更新):Adapter 数据变了,RecyclerView 直接布局新数据 [A, C, D]。C 会瞬间从 B 的位置跳到上面去。由于没有“起点”坐标,动画器不知道 C 是从哪里飞过来的,只能让 B 直接消失,C 原地出现——产生“闪烁”和“跳跃”。
有预布局(Step1):在数据正式更新之前,RecyclerView 先把旧数据 [A, B, C, D] 按原样完整布局一次,并拍下“快照”(记录 A、B、C、D 的精确坐标)。此时,系统知道 “C 的起点在 B 的下面”。
然后进入 Step2(正式布局),数据变成 [A, C, D],C 移动到了 B 原来的位置。系统拍下“第二张快照”。
最后在 Step3 中,ItemAnimator 拿到了 C 的 “起点坐标”和 “终点坐标”,它只需要在这两个坐标之间做插值动画(比如平移动画),C 就会平滑地向上滑动,填补 B 的空缺。
结论:没有预布局,动画器就是“瞎子”,不知道元素从哪来;有了预布局,动画器拥有了“前后对照表”,才能做出流畅的过渡。

  1. 更精妙的一点:预测动画(Predictive Animations)
    你注意到源码里的标志位叫 mRunPredictiveAnimations(预测动画)吗?为什么叫“预测”?
    因为在 Step1(预布局)执行时,Adapter 的数据其实还没有真正更新。RecyclerView 需要“预测”数据更新后屏幕会变成什么样,从而提前告诉那些屏幕上不存在的、但即将出现的 Item(比如删除 B 后,屏幕底部新露出的 Item E)应该从哪里开始出现。
    这种“预判”机制,保证了不仅现有元素移动流畅,新滑入屏幕的元素也能以优雅的动画出现(比如从下方渐入),而不是突兀地蹦出来。
  2. 首次布局
    首次布局没有动画,所以不需要“前后对照”,因此 mRunPredictiveAnimations 为 false,Step1 直接跳过,省下了巨大的性能开销。

非自动测量

方法 setHasFixedSize 性能优化 如果能提前知道RecyclerView的大小不受适配器内容的影响,RecyclerView依然可以更改其size基于其它因素(例如它父View的size)但这种规模的计算不能依赖于 ,其子元素的大小或其适配器的内容(除了适配器中的项数)
如果适配器的更改不会影响RecyclerView的大小那就把它设置为true
于 RecyclerView 尺寸固定(因为fixedSize设置为了true)且已附着窗口,子视图的增删不会影响自身布局尺寸,因此无需立即触发完整的 requestLayout() 重新测量/布局。只需在下一帧绘制前(使用view.post),通过 Runnable 更新子视图的附着/分离状态(例如调用 dispatchLayout() 或 recycleChildren),这样可以 避免多次同步布局请求,将多次更新合并到同一帧,提升滚动或动画过程中的流畅度

 void triggerUpdateProcessor() {
            if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
                ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
            } else {
                mAdapterUpdateDuringMeasure = true;
                requestLayout();
            }
        }

在测量阶段安全、及时地消费适配器更新,并用正确的标记驱动后续的布局和动画流程

if (mAdapterUpdateDuringMeasure) {
}

如果 mAdapterUpdateDuringMeasure是false并且mRunPredictiveAnimations是true 意味着onMeasure已经执行处理了adapter的所有更改,那说明已经进行了一次测量,如果RV是Linearlayout的子View并且 layout_width=match_parent,Rv不能调用 LM(layoutManager).onMeasure第二次,因为 getViewForPosition 会Crash 当LM使用child 去测量时。

为什么直接return,是因为预布局状态下一些ViewHolder可能是已经被移除的,正要进行真正的布局,这个时候获取的一ViewHolder是不稳当,不可靠的,有可能导致崩溃,所以此时还是直接拿之前测量的宽和高来设置RV的宽和高.
自定义 LayoutManager 的重写:开发者继承 LayoutManager 并重写 onMeasure() 时,完全有可能为了测量 WRAP_CONTENT 或特定尺寸,而去调用 getViewForPosition() 来临时创建一个子 View 计算大小。
如果在预布局状态(mInPreLayout = true)下,第二次 onMeasure 调用了这些含有 getViewForPosition 的逻辑,就会崩溃。 原因有两点:
位置索引错乱:预布局状态下,Adapter 数据已更新,但 getItemCount() 返回的可能是旧数量。getViewForPosition(position) 如果拿到的是新数据的 position,可能会超出旧缓存的范围。
ViewHolder 状态异常:预布局阶段,部分 ViewHolder 被标记为“正在被移除”或“预布局专用”(FLAG_UPDATE 或 FLAG_INVALID),此时试图测量它们,状态是脏的,容易触发断言或空指针

else if (mState.mRunPredictiveAnimations) {
 setMeasuredDimension(getMeasuredWidth(), getMeasuredHeight());
                return;
}

RV和其它ViewGroup核心区别是:一般ViewGroup如果到了绘制流程时View树是已经准备好了的,但RV不同,它把添加子View的动作后延到了onMeasure(warp_content AT_MOST)或onLayout(match_parent EXACTLY)阶段因为二者都有可能调用到LM的 onLayoutChildren ,androidx.recyclerview.widget.LinearLayoutManager#fill
在这个方法里有个比较有意思的地方就是 while循环

image.png

重点是性能有关的问题,在非Exactly模式下 layoutstate.mInfinite为true,那么如果layoutState.hasMore(state)一直为ture,则这个循环会一直循环下去,
**androidx.recyclerview.widget.LinearLayoutManager.LayoutState#hasMore **
无论是AT_MOST和Exactly 都会走,hasMore的逻辑, 什么说非Exactly浪费性能? 这里就是答案,因为hasMore里的itemCount就是adapter.getItemCount()
image.png

androidx.recyclerview.widget.RecyclerView.State#getItemCount
image.png

androidx.recyclerview.widget.RecyclerView.Recycler#tryGetViewHolderForPositionByDeadline

对于Scrap 、cache、pool 缓存取出的ViewHolder只要命中else if任何一个条件,都会重新bind,不要以为从Scrap中取得的就一定不会bind的这观念

            boolean bound = false;
            if (mState.isPreLayout() && holder.isBound()) {
                // do not update unless we absolutely have to.
                holder.mPreLayoutPosition = position;
            } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
                if (DEBUG && holder.isRemoved()) {
                    throw new IllegalStateException("Removed holder should be bound and it should"
                            + " come here only in pre-layout. Holder: " + holder
                            + exceptionLabel());
                }
                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
            }

怎么理解dryRun这个参数?
翻译: Does a dry run, finds the ViewHolder but does not remove 做个预演找到VH但不删除它。
怎么理解:如果是true的话 则只看看,不拿走(不占用、不移出缓存)。如果为false的则是要复用了

 /**
         * Returns a view for the position either from attach scrap, hidden children, or cache.
         *
         * @param position Item position
         * @param dryRun   Does a dry run, finds the ViewHolder but does not remove
         * @return a ViewHolder that can be re-used for this position.
         */
        ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
            final int scrapCount = mAttachedScrap.size();

            // Try first for an exact, non-invalid match from scrap.
            for (int i = 0; i < scrapCount; i++) {
                final ViewHolder holder = mAttachedScrap.get(i);
                if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
                        && !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
                    holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
                    return holder;
                }
            }
            //非预演进入判断
            if (!dryRun) {
                View view = mChildHelper.findHiddenNonRemovedView(position);
                if (view != null) {
                    // This View is good to be used. We just need to unhide, detach and move to the
                    // scrap list.
                    final ViewHolder vh = getChildViewHolderInt(view);
                    mChildHelper.unhide(view);
                    int layoutIndex = mChildHelper.indexOfChild(view);
                    if (layoutIndex == RecyclerView.NO_POSITION) {
                        throw new IllegalStateException("layout index should not be -1 after "
                                + "unhiding a view:" + vh + exceptionLabel());
                    }
                    mChildHelper.detachViewFromParent(layoutIndex);
                    scrapView(view);
                    vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP
                            | ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
                    return vh;
                }
            }

            // Search in our first-level recycled view cache.
            final int cacheSize = mCachedViews.size();
            for (int i = 0; i < cacheSize; i++) {
                final ViewHolder holder = mCachedViews.get(i);
                // invalid view holders may be in cache if adapter has stable ids as they can be
                // retrieved via getScrapOrCachedViewForId
                if (!holder.isInvalid() && holder.getLayoutPosition() == position
                        && !holder.isAttachedToTransitionOverlay()) {
                    if (!dryRun) {
                        //如果非预演则移除,因为上面获取的ViewHolder要供上层使用
                        mCachedViews.remove(i);
                    }
                    if (DEBUG) {
                        Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position
                                + ") found match in cache: " + holder);
                    }
                    return holder;
                }
            }
            return null;
        }
类型 是否内存缓存 本质
scrap ✔ 是 “正在布局过程中暂存的 ViewHolder”
hidden children ✔ 是 “仍在 View 层级,但被隐藏的 View”
mCachedViews ✔ 是 “可直接复用的 ViewHolder 缓存池”

为什么 dispatchLayout 、onLayoutChildren时要先 detachAndScrapAttachedViews
因为 LayoutManager 在开始布局前并不知道哪些 ViewHolder 最终还需要,所以先把当前所有子 View detach 并放入 scrap,再由 fill() 按新的布局结果决定哪些重新 attach、哪些最终回收。

 public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
            final int childCount = getChildCount(); //这个count是屏幕高或高size/itemSize宽或高
            for (int i = childCount - 1; i >= 0; i--) {
                final View v = getChildAt(i);
                scrapOrRecycleView(recycler, i, v);
            }
        }

scrapOrRecycleView废弃或者回收视图
1、存入RecyclerPool中
条件:viewHolder.isInvalid(),说明是无效的,意味着不能绑定旧数据
条件:!viewHolder.isRemoved() 说是不是 notifyItemRemoved()产生的
条件:!adapter.hasStableIds() 如果有 StableId:RecyclerView 即使数据刷新,也能根据 id 找回旧 Holder
总结是 如果这个 Holder 已失效,而且没有 StableId 可以重新匹配,就不要 Scrap,直接回收
remove: View直接从ChildHelper删除
detach: View还存在Recycler中
2、进入 Scrap (供本次布局快速复用
deatch后这个View不在View树中了,但View对象还存在,从布局树中脱离

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

再巩固一下获取的流程


image.png

RecyclerView 1.2.1版本缓存分层
Layout开始


① Attached Scrap (layout期间)


② Changed Scrap(PreLayout时才有 动画 )


③ mCachedViews


④ ViewCacheExtension(开发者扩展)


⑤ RecycledViewPool


⑥ Adapter.createViewHolder()

mAttachedScrap:在layout时 scrapOrRecycleView方法中放进 mAttachedScrap,当前屏幕上的马上重新布局的
Changed Scrap:notifyItemChanged 这种 PreLayout 动画期间才存在 保存 旧ViewHolder 用于 ItemAnimator change动画
mCachedViews:View Cache 保存刚刚滑出屏幕的 size的大小是 2 ,已经bind好的VH,不用重新bind 例如 position = 2 向上滑动,再向下滑,可以直接拿出,不需要重新bind,也是在 scrapOrRecycleView中的逻辑。
RecycledViewPool:真正意义上的 回收池里面已经没有Position 只有: Text、Image、Banner、都是分开的池 (准确说已经回收,不再对应Position)如果:viewType一样 直接拿 然后bind 记着一定要bind

RecyclerView回收流程

image.png

image.png

如果只是scrap(只是和RV解除父子关系)或其还挂载在RV上不能回收

if (holder.isScrap() || holder.itemView.getParent() != null) {
    throw ...
}

不归Recycler管理的VH ,shouldIgnore() 为 true 的 ViewHolder,不会进入 Recycler 的 Scrap、mCachedViews、RecycledViewPool 等缓存流程,它仍然属于 RecyclerView,只是暂时不参与 Recycler 的缓存与回收管理

if (holder.shouldIgnore()) {
                throw new IllegalArgumentException("Trying to recycle an ignored view holder. You"
                        + " should first call stopIgnoringView(view) before calling recycle."
                        + exceptionLabel());
            }

前面所有判断都是VH有没有资格进入回收。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容