RecyclerView回收复用分析

作者:Calculus_小王

本文从ViewTraversals三大流程和事件分发讲起,结合使用和体验,重点剖析RecyclerView的回收复用机制。全篇将以LinearLayoutManager为例,围绕RecyclerView.Adapter日常重写的几个经典方法展开,讲清RV的缓存机制

本文篇幅较长,建议读到后面有所遗忘的话,再翻阅下前文

三大阶段:measure、layout、draw

RV(RecyclerView,后文中均如此指代)作为一个ViewGroup,我们理应了解其三大阶段。其中我们重点将放在layout(所以放在最后讲)

measure

在测量阶段,主要关注mLayout.isAutoMeasureEnabled()中的代码段,因为通常的LayoutManager默认开启。对于ViewGroup的测量,我们需要了解child的尺寸,才能决定,尤其对于RV,如LinearLayoutManger.height = WRAP_CONTENT,那么必然需要将child完成摆放后,才能知晓具体尺寸。setMeasuredDimensionFromChildren最终调用了setMeasuredDimension,那么也意味着dispatchLayoutStep2极有可能决定了child item的measure && layout

// RecyclerView.java
LayoutManager mLayout;
protected void onMeasure(int widthSpec, int heightSpec) {
    if (mLayout == null) {
        // 没有LayoutManager
        defaultOnMeasure(widthSpec, heightSpec);
        return;
    }
    if (mLayout.isAutoMeasureEnabled()) {
        // LayoutManager默认开启
        final int widthMode = MeasureSpec.getMode(widthSpec);
        final int heightMode = MeasureSpec.getMode(heightSpec);

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

        mLastAutoMeasureSkippedDueToExact =
                widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
        if (mLastAutoMeasureSkippedDueToExact || mAdapter == null) {
            // 如果尺寸固定EXACTLY或 没有adpater(那也将取不到child),所以测量结束
            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();
        
        // 现在可以真正取到child尺寸了,那也意味着dispatchLayoutStep2极有可能决定了child的摆放和测量
        mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
        // 在dispatchLayout中会进行是否需要二次布局
        mLastAutoMeasureNonExactMeasuredWidth = getMeasuredWidth();
        mLastAutoMeasureNonExactMeasuredHeight = getMeasuredHeight();
    } else {
        if (mHasFixedSize) {
            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
            return;
        }
        // custom onMeasure
    }
}

对于这个阶段,不作过多阐述,需关注一点dispatchLayoutStep2在后续的具体表现。且我们可以推断出,固定尺寸,可以有效加快measure阶段的速度。当然在某些大列表嵌套中,可能是不可避免的

draw

这是RV 三大阶段中唯一有主动重写的发起行为(区别于onXX的响应行为)的。从中我们了解到,ItemDecorations的绘制也在这个阶段介入,也分明了其onDraw和onDrawOver的顺序。当然ItemDecorations的offset设置想必那就在layout阶段了

// RecyclerView.java
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);
    }
    // ……
    if (needsInvalidate) {
        ViewCompat.postInvalidateOnAnimation(this);
    }
}

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

从measure、layout两个阶段,和RV的基本使用中我们认识到,RV将不同的职责划分给了不同的对象(LayoutManager、ItemDecoration、Adapter,其名称对于含义恰如其分,就不多解释了),再在主环节进行针对性介入,是解耦的优秀实践

layout

dispatchLayout中有三个命名几乎一致的方法:dispatchLayoutStep1\2\3,然后对应在Measure阶段也有调用,其中还有个状态标志mState.mLayoutStep,那接着我们带着疑问继续下去:

  1. 三个方法的作用和意义分别是什么
  2. 状态标志如何变化且作用是什么
// RecyclerView.java
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    // 题外话,这就是Trace工具捕捉的方式,begin和end成双入对
    TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
    dispatchLayout();
    TraceCompat.endSection();
    mFirstLayoutComplete = true;
}

void dispatchLayout() {
    if (mAdapter == null) {
        return;
    }
    if (mLayout == null) {
        return;
    }
    boolean needsRemeasureDueToExactSkip = mLastAutoMeasureSkippedDueToExact
                    && (mLastAutoMeasureNonExactMeasuredWidth != getWidth()
                    || mLastAutoMeasureNonExactMeasuredHeight != getHeight());
    mLastAutoMeasureNonExactMeasuredWidth = 0;
    mLastAutoMeasureNonExactMeasuredHeight = 0;
    mLastAutoMeasureSkippedDueToExact = false;

    if (mState.mLayoutStep == State.STEP_START) {
        dispatchLayoutStep1();
        mLayout.setExactMeasureSpecsFrom(this);
        dispatchLayoutStep2();
    } else if (mAdapterHelper.hasUpdates()
            || needsRemeasureDueToExactSkip
            || mLayout.getWidth() != getWidth()
            || mLayout.getHeight() != getHeight()) {
        mLayout.setExactMeasureSpecsFrom(this);
        dispatchLayoutStep2();
    } else {
        mLayout.setExactMeasureSpecsFrom(this);
    }
    dispatchLayoutStep3();
}

同样的,我们调整下阅读顺序,分别为1、3,最后为2

dispatchLayoutStep1

信息1: STEP_START用于阶段1的开始,且结束时设置为STEP_LAYOUT

信息2: 提取的主要信息似乎均和Animation有关,其中两个重要判断mRunSimpleAnimationsmRunPredictiveAnimations均在processAdapterUpdatesAndSetAnimationFlags()中处理,再深入看看吧

信息3: 调用了一次onLayoutChildren,具体详解放在dispatchLayoutStep2,现在只需知道进行了一次布局,通常我们称之为预布局

// RecyclerView.java
private void dispatchLayoutStep1() {
    // 状态校验
    mState.assertLayoutStep(State.STEP_START);
    // ……
    // 这个我们展开看看吧,mInPreLayout预布局标记也在这个阶段设置了
    processAdapterUpdatesAndSetAnimationFlags();
    // ……
    mState.mInPreLayout = mState.mRunPredictiveAnimations;
    // ……

    if (mState.mRunSimpleAnimations) {
        // ……
        mViewInfoStore.addToPreLayout(holder, animationInfo);
        // ……
    }
    if (mState.mRunPredictiveAnimations) {
        // ……
        mLayout.onLayoutChildren(mRecycler, mState);
        // ……
        mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);
        // ……
    }
    // ……
    mState.mLayoutStep = State.STEP_LAYOUT;
}

这块的判断处理非常多,归纳来说:RV支持动画且mItemsAddedOrRemoved || mItemsChanged,即为true。同时mRunPredictiveAnimations依赖于mRunSimpleAnimations

重点: mAdapterHelper.preProcess()是几乎必走的一个方法(暂不考虑去除动画的特殊场景),处理了adapter发生的变化(如add\remove\change),其中mPendingUpdates记录变化的具体行为,调用链可自行摸索(这里不作展开),RecyclerView中的mObserver:RecyclerViewDataObserver,即当调用adpater.notifyXX(任一刷新方式)均会响应,并将对应的行为添加到mPendingUpdates

// RecyclerView.java
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);
        }
    }
    
    if (predictiveItemAnimationsEnabled()) {
        // predictiveItemAnimationsEnabled具体如下,如果不特殊设置去除动画的话,通常为true
        // return (mItemAnimator != null && mLayout.supportsPredictiveItemAnimations());
        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();
}

阶段1总得来说引出了预布局的概念,而预布局和动画的展示(add\remove\change)关联性极大。

唐子玄大佬文中的一句话解释预布局的意义,简单来说就是为动画前留下快照,以进行对比决定动画如何执行

列表中有两个表项(1、2),删除 2,此时 3 会从屏幕底部平滑地移入并占据原来 2 的位置。 为了实现该效果,RecyclerView的策略是:为动画前的表项先执行一次pre-layout,将不可见的表项 3 也加载到布局中,形成一张布局快照(1、2、3)。再为动画后的表项执行一次post-layout,同样形成一张布局快照(1、3)。比对两张快照中表项 3 的位置,就知道它该如何做动画了。

dispatchLayoutStep3

信息1: STEP_ANIMATIONS用于阶段3的开始,且不是必要环节,因为立刻就还原为STEP_START

信息2: mViewInfoStore.process中最终触发了动画。本文对动画细节就不多展开啦,通常称之为后布局postLayout

// RecyclerView.java
private void dispatchLayoutStep3() {
    // ……
    mState.assertLayoutStep(State.STEP_ANIMATIONS);
    // 校验完后,就直接重置了状态标记了
    mState.mLayoutStep = State.STEP_START;
    // ……
    mViewInfoStore.process(mViewInfoProcessCallback);
    // ……
    // scrap回收
    mLayout.removeAndRecycleScrapInt(mRecycler);
}

dispatchLayoutStep2

这是三个阶段中唯一一个可能会多次调用的,同时在这个阶段也确立了views的最终状态

信息1: STEP_LAYOUT用于阶段2的开始,且结束时设置为STEP_ANIMATIONS,由于会多次调用,所以STEP_ANIMATIONS也是开始判断之一

信息2: 区别于measure阶段调用的onLayoutChildren,此时设置了mState.mInPreLayout = false,因此称之为真正布局阶段

// RecyclerView.java
private void dispatchLayoutStep2() {
    // ……
    mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
    // ……
    // Step 2: Run layout
    mState.mInPreLayout = false;
    mLayout.onLayoutChildren(mRecycler, mState);
    // ……
    mState.mLayoutStep = State.STEP_ANIMATIONS;
    // ……
}

下文中关于mLayout,均以LinearLayoutManager为例,首先detachAndScrapAttachedViews将RV中现有attached views全部解绑并废弃,当然这个废弃是临时的(详解见Fill章节),目的是为了避免如预布局阶段为事前快照造成的影响而进行的恢复画布。然后进行fill填充,这里带入了isPreLayout,后面会再提到。

// LinearLayoutManager.java
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    // ……
    detachAndScrapAttachedViews(recycler);
    mLayoutState.mIsPreLayout = state.isPreLayout();
    // ……
    fill(recycler, mLayoutState, state, false);
    // ……
    // 这里只有真正布局阶段才能进入,会将剩余的mAttachedScrap填充到屏幕中
    layoutForPredictiveAnimations(recycler, state, startOffset, endOffset);
}

dispatchLayoutStep总结

由此,我们大致知晓了各阶段的意义和作用,且状态标记用于阶段流转的处理(需注意measure阶段也是会调用的喔)

RecyclerView回收复用分析_第1张图片

滑动事件

RV在使用过程中,滑动是最为频繁的,而滑动相关的ACTION_MOVE自然不能放过。其中scrollByInternal是其自身进行滑动消费的具体逻辑,随着代码调用链,最终走到了LinearLayoutManager.scrollBy中,其中更有新发现,居然也调用了fill(前面讲layoutChildren时的具体RV填充)。

可以试想一下: 在手指上滑过程中,顶部的item被逐步移除,底部新的item被添加。那fill中必然涉及回收、复用两个环节,这也是我们将它独立成章节的原因

// RecyclerView.java
public boolean onTouchEvent(MotionEvent e) {
    // ……
    case MotionEvent.ACTION_MOVE: {
        // 这里作了多指兼容,并不是指多指手势,而具体指 当多个手指down下时,最终仅以最后一个手指为move基准
        final int index = e.findPointerIndex(mScrollPointerId);
        if (index < 0) {
            Log.e(TAG, "Error processing scroll; pointer index for id "
                    + mScrollPointerId + " not found. Did any MotionEvents get skipped?");
            return false;
        }
        // ……
        // 嵌套滑动,优先询问parent
        dispatchNestedPreScroll(
            canScrollHorizontally ? dx : 0,
            canScrollVertically ? dy : 0,
            mReusableIntPair, mScrollOffset, TYPE_TOUCH
        )
        // ……
        // 自己消费剩下的
        scrollByInternal(
            canScrollHorizontally ? dx : 0,
            canScrollVertically ? dy : 0,
            e, TYPE_TOUCH)
        // ……
    }
    break;
    // ……
    return true;
}

boolean scrollByInternal(int x, int y, MotionEvent ev, int type) {
    // ……
    scrollStep(x, y, mReusableIntPair);
    // ……
}

void scrollStep(int dx, int dy, @Nullable int[] consumed) {
    // ……
    if (dx != 0) {
        consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
    }
    if (dy != 0) {
        // 下面以纵向举例
        consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
    }

    // ……
}

// LinearLayoutManager.java
// 注意,这里类不一样了喔
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
        RecyclerView.State state) {
    // ……
    return scrollBy(dx, recycler, state);
}

int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
    // ……
    final int consumed = mLayoutState.mScrollingOffset
            + fill(recycler, mLayoutState, state, false);
    // ……
}

Fill 回收-复用

这将是RV讲解中的重中之重,当然不是所有的LayoutManager都有这个方法,只是举例的LinearLayoutManager中onLayoutChildrenscrollVerticallyBy最终都走向了该方法。而其源点,应是layoutonTouchEvent.ACTION_MOVE即三大流程和事件分发

// LinearLayoutManager.java
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
        RecyclerView.State state, boolean stopOnFocusable) {
    // ……
    // 剩余空间,循环过程中会不断减去新增item的尺寸,直到剩余空间不足结束
    int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
        // ……
        // 填充,会优先尝试复用
        layoutChunk(recycler, state, layoutState, layoutChunkResult);
        // ……
        // 当不需要ignore 或 不是预布局 或 正在摆放scrap时需要进行剩余空间计算
        // 换言之,当且仅当 预布局阶段,mIgnoreConsumed标记为true的item会被跳过,因此会有多加载一个的情况
        // 如【1、2】,remove 2,那么事前快照就应该需要【1、2、3】,对比事后【1、3】才能匹配动画
        if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
                || !state.isPreLayout()) {
            layoutState.mAvailable -= layoutChunkResult.mConsumed;
            // we keep a separate remaining space because mAvailable is important for recycling
            remainingSpace -= layoutChunkResult.mConsumed;
        }

        // 如向上滑动过程中,顶部的Item会被逐步回收
        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;
}

主要关注两个方法,layoutChunkrecycleByLayoutState,关于剩余空间的概念应该很好理解,从方法顺序可以看出,先填充后移除。可以思考一下为什么不是先移除后填充?

假设填充基于复用,移除基于回收,如果先移除后填充,那么我原先预期可以被先复用的缓存,可能被先移除加入缓存的view顶替掉,那么回收-复用就不那么高效了

缓存

回收-复用这个话题通常都伴随着缓存这个概念,如Glide的活动缓存、内存缓存、磁盘缓存三级缓存Message.obtain的享元池化,缓存是回收的去处,复用的来源,是一种手段、一种媒介。而多级缓存则是为了提高复用的效率,减少回收的成本

剧透下RV所用缓存的数据结构,以结果导向,避免在后续的行文中迷茫,当然在下文中也会再次强化概念。缓存的对象均为ViewHolder

对于三级缓存还是四级缓存,众说纷纭,因为官方注释中mCachedViews是first-level cache

RecyclerView回收复用分析_第2张图片

// RecyclerView.Recycler.java
// 两个Scrap(废弃)缓存,这个废弃为临时废弃之意,是预布局后为恢复画布而进行detach后快速布局使用,区别为changed是否污染,即需要bindView
final ArrayList mAttachedScrap = new ArrayList<>();
ArrayList mChangedScrap = null;

// cache缓存,是RecycledViewPool的预备队列,默认size为2,可通过setViewCacheSize设置阈值
// 奉行先入先出原则,所以后来的缓存会顶替最老的缓存
static final int DEFAULT_CACHE_SIZE = 2;
int mViewCacheMax = DEFAULT_CACHE_SIZE;
final ArrayList mCachedViews = new ArrayList();

// 自定义缓存扩展,较少使用
private ViewCacheExtension mViewCacheExtension;

// 缓存池,每个viewType默认缓存5个,可通过setMaxRecycledViews进行扩容
RecycledViewPool mRecyclerPool;

public static class RecycledViewPool {
    private static final int DEFAULT_MAX_SCRAP = 5;

    static class ScrapData {
        final ArrayList mScrapHeap = new ArrayList<>();
        int mMaxScrap = DEFAULT_MAX_SCRAP;
    }
    SparseArray mScrap = new SparseArray<>();
    
    public ViewHolder getRecycledView(int viewType) {
        final ScrapData scrapData = mScrap.get(viewType);
        if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
            final ArrayList scrapHeap = scrapData.mScrapHeap;
            for (int i = scrapHeap.size() - 1; i >= 0; i--) {
                if (!scrapHeap.get(i).isAttachedToTransitionOverlay()) {
                    return scrapHeap.remove(i);
                }
            }
        }
        return null;
    }
}

在正式进入回收-复用时,请带着这样的思考

缓存优先级是不是意味着,对应的复用性能也是从高到低?(复用性能越好意味着所做的昂贵操作越少)

1.最坏情况:重新创建ViewHodler并重新绑定数据

2.次好情况:复用ViewHolder但重新绑定数据

3.最好情况:复用ViewHolder且不重新绑定数据

引文来自-作者:唐子玄

回收

关于回收,应该分成两部分看待

1. 由于前文中提到“会存在 预布局中为了获取事前快照污染了画布”,因此在fill前,进行了一次detachAndScrapAttachedViews(recycler)(在onLayoutChildren中) 的暂时废除。发现两个回收方法recycleViewHolderInternalscrapView

// RecyclerView.java
public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
    final int childCount = getChildCount();
    for (int i = childCount - 1; i >= 0; i--) {
        final View v = getChildAt(i);
        scrapOrRecycleView(recycler, i, v);
    }
}

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()) {
        // 如果设置了hasStableIds,那也会放入scrap
        removeViewAt(index);
        recycler.recycleViewHolderInternal(viewHolder);
    } else {
        detachViewAt(index);
        recycler.scrapView(view);
        mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
    }
}

2. fill填充过程中伴随着recycleByLayoutState移除的回收行为。跟随调用链recycleByLayoutState >>> recycleViewsFromEnd(Start也是一样) >>> recycleChildren >>> RecyclerView.removeAndRecycleViewAt >>> recycleView,最终也发现了recycleViewHolderInternal

// LinearLayoutManager.java
private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
    if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
        recycleViewsFromEnd(recycler, scrollingOffset, noRecycleSpace);
    } else {
        recycleViewsFromStart(recycler, scrollingOffset, noRecycleSpace);
    }
}

private void recycleViewsFromEnd(RecyclerView.Recycler recycler, int scrollingOffset,
        int noRecycleSpace) {
    // ……
    final int limit = mOrientationHelper.getEnd() - scrollingOffset + noRecycleSpace;
    // ……
    if (mOrientationHelper.getDecoratedStart(child) < limit
            || mOrientationHelper.getTransformedStartWithDecoration(child) < limit) {
        // stop here
        recycleChildren(recycler, 0, i);
        return;
    }
    // ……
}

public void recycleView(@NonNull View view) {
    // ……
    recycleViewHolderInternal(holder);
    // ……
}

关于limit的理解可以参考鸿洋大佬文中的这张图,我直接搬运来了

RecyclerView回收复用分析_第3张图片

3. 既然recycleViewHolderInternal两次都有调用,那么老规矩我们先讲scrapView。我们发现哪怕是Scrap,也进行了mAttached和mChanged区分,同时对ViewHolder进行绑定mScrapContainer

像remove是走mAttached,但change即update(上文dispatchLayoutStep1中提到mAdapterHelper.preProcess(),处理了存储在mPendingUpdates中的状态变化,而状态则由mObserver:RecyclerViewDataObserver监听发送)是走入mChanged的。

由此:我们得知两个scrap一级缓存的作用是一致的,但需要区分数据是否污染(需要更新即认为污染了)

// RecyclerView.Recycler.java
void scrapView(View view) {
    final ViewHolder holder = getChildViewHolderInt(view);
    if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
            || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
        // ……
        holder.setScrapContainer(this, false);
        mAttachedScrap.add(holder);
    } else {
        // ……
        holder.setScrapContainer(this, true);
        mChangedScrap.add(holder);
    }
}

4. recycleViewHolderInternal就比较类似于我们日常的回收理念了,其中isRecyclable设置了false就不回收了,会严重影响性能,也就意味着每次都会new create。所以对于回收复用带来的可能的数据渲染错乱问题(不是RV的问题,是你的问题),应该是通过在onBindViewHolder中对if-else条件进行对应渲染行为才对。

注意重点条件:!holder.hasAnyOfTheFlags,也就是说二级mCachedViews中的缓存应该是可以直接使用,而无须re-bind的。同时当mCachedViews溢出时,最老的item将会通过recycleCachedViewAt回收到mRecyclerPool中,即先入先出的队列特性,所以可以认为mCachedViews是mRecyclerPool的预备队列,且其缓存是无污染的

// RecyclerView.Recycler.java
void recycleViewHolderInternal(ViewHolder holder) {
    // ……
    boolean cached = false;
    boolean recycled = false;
    // ……
    if (forceRecycle || holder.isRecyclable()) {
        if (mViewCacheMax > 0
                && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                | ViewHolder.FLAG_REMOVED
                | ViewHolder.FLAG_UPDATE
                | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
            // Retire oldest cached view
            int cachedViewSize = mCachedViews.size();
            if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                recycleCachedViewAt(0);
                cachedViewSize--;
            }
            // ……
            mCachedViews.add(targetCacheIndex, holder);
            cached = true;
        }
        if (!cached) {
            addViewHolderToRecycledViewPool(holder, true);
            recycled = true;
        }
    } else {
         // ……
    }
    // ……
}

回收的内容大致就这么多,回顾一下好像没有用到mViewCacheExtension!!!虽然复用时会进行尝试取,但回收流程中,并没有将其介入,即完全由开发者自行维护(oh no~)。

然后还明白了两个区分:

  1. 废弃scrap和缓存
  2. 污染情况

下面将从复用角度讲讲为什么废弃是临时性的

复用

layoutChunk 中完成了

// LinearLayoutManager.java
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
        LayoutState layoutState, LayoutChunkResult result) {
    View view = layoutState.next(recycler);
    // ……
    // 实际为attach,即建立child 和 RV 的绑定关系
    addView\addDisappearingView(view);
    // 测量
    measureChildWithMargins(view, 0, 0);
    // 测量完后就知道需要消耗多少空间了,remainingSpace的计算依赖于此
    result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
    // 当然,这里的l\t\r\b计算也包含了ItemDecoration的附加
    // measure完之后就layout,老生常谈
    layoutDecoratedWithMargins(view, left, top, right, bottom);
    if (params.isItemRemoved() || params.isItemChanged()) {
        // 这里是前面提到过的,预布局阶段计算剩余空间会跳过的场景,即会多加载下一个item
        result.mIgnoreConsumed = true;
    }
}

复用来自于填充时的需要,从layoutChunk逐步向下(layoutState.next >>> recycler.getViewForPosition >>> tryGetViewHolderForPositionByDeadline),我们找到了tryGetViewHolderForPositionByDeadline。这里一共进行了5次尝试取缓存,其中也有我们所熟悉的两个Adapter方法createViewHolder>>>onCreateViewHoldertryBindViewHolderByDeadline>>>onBindViewHolder

  1. 预布局
  2. 根据Position取
  3. 根据Id和ViewType取
  4. 自定义缓存
  5. 从缓存池根据ViewType取
  6. 创建ViewHolder

鉴于篇幅已经较长,关于各个方法内部的逻辑可自行深入去看,便不再逐一展开了

// RecyclerView.java
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
        boolean dryRun, long deadlineNs) {
    // ……
    boolean fromScrapOrHiddenOrCache = false;
    ViewHolder holder = null;
    // 1. 仅用于预布局中的change,为了和mAttached区分,因为changed为执行动画前后需要不同的ViewHolder
    if (mState.isPreLayout()) {
        holder = getChangedScrapViewForPosition(position);
    }
    // 2.根据Position取,依次从 mAttachedScrap、mHiddenViews(这个忘了提-_-,涉及到动画)、mCachedViews
    // 会进行严格校验,如果失效了,会被再次回收
    holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
    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;
    } 
    // 3.根据Id和ViewType取,前提设置了hasStableIds,这里呼应了回收时,如果设置了也会相应回收进scrap,但仅对于detach场景
    if (mAdapter.hasStableIds()) {
        // 从 mAttachedScrap、mCachedViews取
        holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
        holder.mPosition = offsetPosition;
    }
    // 4.自定义缓存
    final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
    holder = getChildViewHolder(view);
    // 5. 从缓存池根据ViewType取
    holder = getRecycledViewPool().getRecycledView(type);
    // 这个重置操作很重要,意味着它必然需要re-bind。因为flag置0,下面!holder.isBound()一定为true
    holder.resetInternal();
    // 6.创建ViewHolder
    holder = mAdapter.createViewHolder(RecyclerView.this, type);
    // …… 
    if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
        // 不走onBind也就是说Position不会更新
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        // 此处的判断决定了什么情况需要re-bind
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }

    // ……
    return holder;
}

2.getScrapOrHiddenOrCachedHolderForPosition中,进行了positionInvalid有效性校验,同时mAttachedScrapmCachedViews在回收时也是校验过非Update的,因此可以推断此处取出后,不需要进行re-bind。(备注:mHiddenViews本文不作关注,与动画相关)

那么结合回收时的策略,那么推测mAttachedScrap用于detach后的快速恢复(那remove掉,最后去了哪呢?其实在dispatchLayoutStep3中removeAndRecycleScrapInt进行了最后的回收,会到缓存池喔),而mCachedViews应用于上下反复滑动时,比如【1、2、3】,向上滑动【2、3、4】,再向下滑动【1、2、3】时的直接复用

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())) {
            // scrap被取用后,不是remove掉,而是加了标记位。判断!holder.wasReturnedFromScrap()也是为了避免重复取用
            holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
            return holder;
        }
    }

    // ……

    // 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) {
                mCachedViews.remove(i);
            }
            if (DEBUG) {
                Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position
                        + ") found match in cache: " + holder);
            }
            return holder;
        }
    }
    return null;
}

对于1.getChangedScrapViewForPosition,为什么仅用于预布局中?因为此时的changed并不代表需要re-bind,仅仅意味着之后会update,用于代表一种“事前”状态,而真正布局阶段,需要changed的item会复用recyclerPool中的缓存,即取一个新的ViewHolder,并进行re-bind和真正的测量布局,代表一种“事后”状态。既然如此,那么为什么不放在mAttachedScrap中呢?因为在真正布局阶段,剩余没被layout的mAttachedScrap将在onLayoutChildren的最后调用layoutForPredictiveAnimations,以将剩下的全部layout,目的我举个例子就知道了。比如屏幕只能容纳两个item,【1,2】,需要insert在1~2之间,那么【1,new2,old2】才是“事前”快照的真正模样,而mChangedScrap则避免了在该阶段被填补上去

这一段可能有点绕,建议多读几遍,我也不确定这段理解是否正确,欢迎大佬们指正

if (mState.isPreLayout() && holder.isBound()) {
    // do not update unless we absolutely have to.
    holder.mPreLayoutPosition = position;
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
    ……
    bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}

还有3.getScrapOrCachedViewForId的意义在于哪里?hasStableIds的使用是对getChangedScrapViewForPosition的一种补充。同时,在notifyDataSetChanged的观察调用链中发现hasStableIds可避免在此场景下的mCachedViews的回收(此时的缓存是污染了的,这点很特殊,需要注意,即复用需要re-bind)。在一些特定场景上一定程度上确实提升复用的有效性和效率

void markKnownViewsInvalid() {
    final int cachedCount = mCachedViews.size();
    for (int i = 0; i < cachedCount; i++) {
        final ViewHolder holder = mCachedViews.get(i);
        if (holder != null) {
            holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID);
            holder.addChangePayload(null);
        }
    }

    if (mAdapter == null || !mAdapter.hasStableIds()) {
        // we cannot re-use cached views in this case. Recycle them all
        recycleAndClearCachedViews();
    }
}

灵魂发问

1. RV缓存扩容何时使用

以下建议基于本人阅读源码后的一些猜想,并未经过试验得出优化效果,欢迎大家交流。具体如何使用,需要结合实际场景和交互来评估

以盒马分类的二级分类栏为例,当切换一级分类时,需要刷新渲染全部二级分类,认定全部均为污染viewholder,作为同一ViewType,recyclerPool中仅默认缓存5个,对于图中接近10个来说,根本不够。基于数据足够的情况下,几乎每次切换都需要create viewholder约5次。那么把该type的缓存扩大(setMaxRecycledViews),那么仅需要re-bind即可

RecyclerView回收复用分析_第4张图片

mCachedViews什么时候推荐扩容呢,我个人猜想了一下,或许像瀑布流、宫格这种,上滑一行移动可能会有多个被同时移除的,再下滑一行可以进行快速恢复(像宫格,被默认设置了2+spanCount),而避免因溢出而部分被回收到recyclerPool中进行re-bind

2. 什么情况下取出的缓存需要onBind,什么时候不需要?

前面我们讲到从RecycledViewPool中取出的需要进行resetInternal重置,必然需要进行re-bind。以及mChangedScrap确认需要update才得以区分,所以也需要re-bind

那剩下mAttachedScrapmCachedViews都是经过判断不含FLAG_UPDATE,自然也就不需要了。当然这也是它们的局限性,因为仅能用于相同position的复用

if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
    bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}

3. RV其他的一些优化建议

  1. 如果RV的尺寸固定,可通过RecyclerView.setHasFixedSize(true)避免不必要的requestLayout消耗性能
// RecyclerViewDataObserver.java
void triggerUpdateProcessor() {
    if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
        RecyclerView.this.postOnAnimation(mUpdateChildViewsRunnable);
    } else {
        mAdapterUpdateDuringMeasure = true;
        requestLayout();
    }
}
  1. 移除默认动画(根据情况决定)
  2. onBindViewHolder尽可能仅bind,减少处理数据的情况
  3. 使用payload局部刷新,减少不必要的onBindViewHolder的其他数据绑定,可结合DiffUtil
  4. 监听滑动,滑动时取消如图片加载等操作,如Glide.with(mContext).pauseRequests()

4. notifyDataSetChanged有多不建议

调用了markKnownViewsInvalid方法所有的ViewHolder标记为了FLAG_INVALID,那就意味着detach环节,所有item将全部被回收到recyclerViewPool中,如果池子溢出,那fill时就需要create造成消耗

@Override
public void onChanged() {
    assertNotInLayoutOrScroll(null);
    mState.mStructureChanged = true;

    processDataSetCompletelyChanged(true);
    if (!mAdapterHelper.hasPendingUpdates()) {
        requestLayout();
    }
}

void processDataSetCompletelyChanged(boolean dispatchItemsChanged) {
    mDispatchItemsChangedEvent |= dispatchItemsChanged;
    mDataSetHasChangedAfterLayout = true;
    markKnownViewsInvalid();
}

Android 学习笔录

Android 性能优化篇:https://qr18.cn/FVlo89
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android 音视频篇:https://qr18.cn/Ei3VPD
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap

你可能感兴趣的:(移动开发,Android,架构,android,移动开发,安卓,性能优化,framework,App架构)