RecyclerView回收复用分析

news2024/12/23 9:18:50

作者: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阶段也是会调用的喔)

滑动事件

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

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

// 自定义缓存扩展,较少使用
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<ViewHolder> mScrapHeap = new ArrayList<>();
        int mMaxScrap = DEFAULT_MAX_SCRAP;
    }
    SparseArray<ScrapData> mScrap = new SparseArray<>();
    
    public ViewHolder getRecycledView(int viewType) {
        final ScrapData scrapData = mScrap.get(viewType);
        if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
            final ArrayList<ViewHolder> 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的理解可以参考鸿洋大佬文中的这张图,我直接搬运来了

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即可

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/954793.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

文献阅读:Deep Learning Enabled Semantic Communication Systems

目录 论文简介关于文章内容的总结引申出不理解的问题 论文简介 作者 Huiqiang Xie Zhijin Qin Geoffrey Ye Li Biing-Hwang Juang 发表期刊or会议 《IEEE TRANSACTIONS ON SIGNAL PROCESSING》 发表时间 2021.4 这篇论文由《Deep Learning based Semantic Communications: A…

网络有源号角(50W-100W)社区小区广播 工地语音播报,隧道广播,钢铁广播广播系统

网络有源号角&#xff08;50W-100W&#xff09;社区小区广播 工地语音播报&#xff0c;隧道广播&#xff0c;钢铁广播广播系统 SV-7042T 50W网络有源号角 SV-7042T是深圳锐科达电子有限公司的一款壁挂式网络有源号角&#xff0c;具有10/100M以太网接口&#xff0c;可将网络音…

SSM - Springboot - MyBatis-Plus 全栈体系(三)

第二章 SpringFramework 一、技术体系架构 1. 总体技术体系 1.1 单一架构 一个项目&#xff0c;一个工程&#xff0c;导出为一个war包&#xff0c;在一个Tomcat上运行。也叫all in one。 单一架构&#xff0c;项目主要应用技术框架为&#xff1a;Spring , SpringMVC , Myba…

经典文献阅读之--FastSAM(快速分割一切)

0. 简介 MetaAI提出的能够“分割一切”的视觉基础大模型SAM提供了很好的分割效果&#xff0c;为探索视觉大模型提供了一个新的方向。虽然SAM的效果很好&#xff0c;但由于SAM的backbone使用了ViT&#xff0c;导致推理时显存的占用较多&#xff0c;推理速度偏慢&#xff0c;对硬…

隧道结构健康监测系统,保障隧道稳定安全运行

隧道是地下隐蔽工程&#xff0c;会受到潜在、无法预知的地质因素影响&#xff0c;早期修建的隧道经常出现隧道拱顶开裂、地表沉降、隧道渗漏水、围岩变形、附近建筑物倾斜等隧道的健康问题变得日益突出&#xff0c;作为城市生命线不可或缺的一部分&#xff0c;为了确保隧道工程…

word中标题及公式自动编号

word中公式自动编号 1. 实现目标2. 详细步骤2.1 添加自动编号功能2.2 输入标题并编号2.3 新建公式2.3.1 编辑公式2.3.4 公式编号的交叉引用2.3.5 公式位置变动以及更新正文中的编号 在word中自动编号公式一直是一个老大难问题&#xff0c;现在通过总结网友们提供的方法&#xf…

3D风速仪 Gill Instruments Limited_R3-50 R3-100 and R3A -100 Manual

R3测量超声波脉冲从上部换能器到相反的下部换能器所花费的时间&#xff0c;并将其与脉冲从下部换能器到上部换能器的时间进行比较。 同样&#xff0c;在其他上下换能器之间比较时间。 如图1所示&#xff0c;每对换能器之间沿轴的空气速度可以从每条轴上的飞行次数计算出来。 …

『PyQt5-Qt Designer篇』| 06 Qt Designer中水平布局和垂直布局的使用

06 Qt Designer中水平布局和垂直布局的使用 1 水平布局1.1 按钮布局1.2 位置移动1.3 先布局再放按钮1.4 保存文件并调用2 垂直布局2.1 按钮布局2.2 保存并调用1 水平布局 1.1 按钮布局 拖动几个按钮: 选中这几个按钮,右键-布局-水平布局: 可以看到按钮间隔等宽水平排列: 也…

【重要】【转载】NOR Flash芯片内执行(XIP)

为什么程序不能直接在nandflash上执行&#xff1f;出于这个疑惑带来了这篇博文&#xff0c;是我在网上找了很多资料后总结的&#xff0c;假如有误&#xff0c;希望马上指出来&#xff0c;免得我误人子弟。谢谢&#xff01; 首先认识下nandflash和norflash&#xff1a; NOR Flas…

android 实现本地一键打包,告别繁琐的studio操作

前言 在实际开发项目中&#xff0c;我们的工程目录往往是多个app在一个工程下的&#xff0c;每次打包都需要手动的用studio点击Build->Generate Signed Bundle or APK->APK 选择app&#xff0c;签名等&#xff0c;甚至有的app签名还不一样&#xff0c;还需要手动的来回切…

java 多线程编程 CountDownLatch(线程计数器) 和 CyclicBarrier 的用法

CountDownLatch - 线程计数器 包名&#xff1a;java.util.concurrent 功能&#xff1a; 多线程编程中&#xff0c;要并发请求10个接口&#xff0c;等这些接口都返回结果再进行统一处理后&#xff0c;将结果返回。 调用countDown() 方法 &#xff0c;计数减去 1。 代码示例…

深度学习(前馈神经网络)知识点总结

用于个人知识点回顾&#xff0c;非详细教程 1.梯度下降 前向传播 特征输入—>线性函数—>激活函数—>输出 反向传播 根据损失函数反向传播&#xff0c;计算梯度更新参数 2.激活函数(activate function) 什么是激活函数&#xff1f; 在神经网络前向传播中&#x…

【Linux系列】离线安装openjdk17的rpm包

首发博客地址 首发博客地址[1] 系列文章地址[2] 视频地址[3] 准备 RPM 包 请从官网下载&#xff1a;https://www.oracle.com/java/technologies/downloads/#java17[4] 如需不限速下载&#xff0c;请关注【程序员朱永胜】并回复 1020 获取。 安装 yum localinstall jdk-17_linux…

renren-fast-vue环境升级后,运行正常打包后,访问页面空白

网上各种环境&#xff0c;路径都找了一遍&#xff0c;也没成功。后来发现升级后打包的dist文件结构发生了变化&#xff0c; 1.最开始正常版本是这样 2.升级后是这样&#xff0c;少了日期文件夹 3.问题&#xff1a;打包后的index.html中引入的是config文件夹&#xff0c;而打…

Matlab(画图进阶)

目录 大纲 1.特殊的Plots 1.1 loglog(双对数刻度图) ​1.3 plotyy(创建具有两个y轴的图形) 1.4yyaxis(创建具有两个y轴的图) 1.5 bar 3D条形图(bar3) 1.6 pie(饼图) 3D饼图 1.7 polar 2.Stairs And Ste阶梯图 3.Boxplot 箱型图和Error Bar误差条形图 3.1 boxplot 3.2 …

国外地址如何地理编码?Python三行代码解决!

对于单个地址基于Python中的geocoder库获取经纬度非常方便&#xff0c;代码如下&#xff1a; # codingutf-8 import geocoder # 输入地址 address Akala Temple lonLat geocoder.osm(address) print(lonLat.latlng) # 输出结果 [27.8569644, 84.0893767]若地址数量较多&am…

Gradio项目所学

从项目中学习Gradio&#xff1a;快速搭建AI算法可视化部署演示(侧重项目搭建与案例分享) 我们通常使用的两款AI可视化交互应用&#xff1a; Gradio 与 StreamList 本次我们通过项目案例对Gradio从简单使用到进阶进行讲解介绍 Gradio的优势 Gradio的优势在于其代码结构的简单性与…

Java“牵手”京东商品详情数据,京东API接口申请指南

京东平台商品详情接口是开放平台提供的一种API接口&#xff0c;通过调用API接口&#xff0c;开发者可以获取京东商品的标题、价格、库存、月销量、总销量、库存、详情描述、图片等详细信息 。 获取商品详情接口API是一种用于获取电商平台上商品详情数据的接口&#xff0c;通过…

面试总结 - 计算机网络

计算机网络 1 OSI 七层模型 | TCP与UDP | 响应状态码 OSI 模型 应用层: 计算机用户&#xff0c;以及各种应用程序和网络之间的接口&#xff0c;其功能是直接向用户提供服务&#xff0c;完成用户希望在网络上完成的各种工作。 HTTP SMTP FTP DNS 表示层: 负责数据格式的转换&…

鸿蒙系列-如何更好地使用 ArkUI 的 Image 组件?

如何使用好 ArkUI 的 Image 组件&#xff1f; 开发者经常需要在应用中显示一些图片&#xff0c;例如&#xff1a;按钮中的logo、网络图片、本地图片等。在应用中显示图片需要使用 Image 组件实现&#xff0c;Image支持多种图片格式&#xff0c;包括png、jpg、bmp、svg和gif&am…