Análise de reciclagem e reutilização RecyclerView

Autor: Cálculo_Xiao Wang

Este artigo começa com os três principais processos e distribuição de eventos do ViewTraversals, combina uso e experiência e se concentra na análise do mecanismo de reciclagem e reutilização do RecyclerView. O artigo inteiro tomará LinearLayoutManager como exemplo, girará em torno de vários métodos clássicos de reescrita diária de RecyclerView.Adapter e explicará o mecanismo de cache de RV

Este artigo é bastante longo, então sugiro que você o leia mais tarde, caso tenha esquecido alguma coisa, e depois leia o artigo anterior

Três etapas: medir, traçar, desenhar

RV (RecyclerView, referido no texto a seguir) como ViewGroup, devemos entender seus três principais estágios. Dentre eles, vamos focar no layout (por isso falaremos sobre isso no final)

medir

Na fase de medição, o foco principal está mLayout.isAutoMeasureEnabled()no segmento de código, pois o LayoutManager normal está habilitado por padrão. Para a medição do ViewGroup, precisamos saber o tamanho da criança antes de podermos decidir, especialmente para RV. Por exemplo LinearLayoutManger.height = WRAP_CONTENT, o tamanho específico deve ser conhecido após a colocação da criança. setMeasuredDimensionFromChildrenFinalmente chamado setMeasuredDimension, também significa que dispatchLayoutStep2é muito provável que determine a medida && layout do item filho

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

Para esta etapa, sem muita elaboração, precisamos nos atentar ao dispatchLayoutStep2desempenho específico no acompanhamento. E podemos inferir que o tamanho fixo pode efetivamente acelerar a fase de medição. É claro que em alguns aninhamentos de listas grandes, isso pode ser inevitável

empate

Esta é a única entre as três fases principais do RV que possui um comportamento de iniciação de reescrita ativa (diferente do comportamento de resposta de onXX). A partir disso, aprendemos que ItemDecorationso desenho de também está envolvido nesta etapa, e a ordem de seu onDraw e onDrawOver também é clara. Claro, a configuração de deslocamento de ItemDecorations deve estar no layoutestágio

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

A partir das duas fases de medida e layout, e do uso básico do RV, percebemos que o RV divide diferentes responsabilidades para diferentes objetos (LayoutManager, ItemDecoration, Adapter, cujos nomes são adequados ao significado, então não vou explicar muito), e então a intervenção direcionada no elo principal é uma excelente prática de dissociação

disposição

Existem dispatchLayouttrês métodos com quase o mesmo nome: dispatchLayoutStep1\2\3, e depois há chamadas correspondentes na fase Measure, que também possui um sinalizador de status mState.mLayoutStep, então continuamos com as perguntas:

  1. Quais são as funções e o significado dos três métodos
  2. Como os sinalizadores de status mudam e o que eles fazem
// 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();
}

Da mesma forma, ajustamos a ordem de leitura para 1, 3 e finalmente 2

expediçãoLayoutStep1

Informação 1: STEP_START Usado no início da fase 1 e definido comoSTEP_LAYOUT

Informação 2: As principais informações extraídas parecem estar Animationrelacionadas, e os dois julgamentos importantes mRunSimpleAnimations e mRunPredictiveAnimations são todos processAdapterUpdatesAndSetAnimationFlags()processados, vamos dar uma olhada mais profunda

Informação 3: Chamado uma vez onLayoutChildren, os detalhes específicos são liberados em dispatchLayoutStep2 , agora só falta saber que foi realizado um layout, normalmente chamamos预布局

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

Há muitos julgamentos e processamentos nesta área. Em resumo: RV suporta animação e mItemsAddedOrRemoved || mItemsChangedé verdadeiro. também mRunPredictiveAnimationsdependemRunSimpleAnimations

Ponto-chave: mAdapterHelper.preProcess() é um método quase necessário (a cena especial de remoção de animação não é considerada por enquanto) e trata das alterações do adaptador (como adicionar\remover\alterar), que registra o comportamento específico da mudança, e a cadeia de chamadas pode ser explorada por si só (não feito mPendingUpdatesaqui Expandir), em RecyclerView mObserver:RecyclerViewDataObserver, ou seja, quando adaptador.notifyXX for chamado (qualquer método de atualização), ele responderá e o comportamento correspondente será adicionado amPendingUpdates

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

De modo geral, a etapa 1 leva ao 预布局conceito, e o pré-layout e a exibição da animação (adicionar\remover\alterar) são extremamente relevantes.

唐子玄O significado de uma frase explicada no artigo citado pelo irmão mais velho 预布局é simplesmente deixar um instantâneo antes da animação para comparação para determinar como a animação será executada

Existem dois itens (1, 2) na lista, exclua 2, agora 3 se moverá suavemente da parte inferior da tela e ocupará a posição original dos 2. Para conseguir esse efeito, a estratégia do RecyclerView é: primeiro realizar um pré-layout para o item da tabela antes da animação e carregar o item invisível da tabela 3 no layout para formar um instantâneo do layout (1, 2, 3). Em seguida, execute um pós-layout para o item da tabela após a animação e também forme um instantâneo do layout (1, 3). Compare a posição da entrada 3 nos dois instantâneos e você saberá como animá-la.

expediçãoLayoutStep3

Mensagem 1: STEP_ANIMATIONS Utilizada no início da fase 3, e não necessária, pois reverte imediatamente paraSTEP_START

Mensagem 2: mViewInfoStore.process A animação é finalmente acionada no meio. Este artigo não irá expandir os detalhes da animação, que geralmente é chamada de pós-layoutpostLayout

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

expediçãoLayoutStep2

Esta é a única das três fases que pode ser chamada várias vezes e também estabelece o estado final das visualizações durante esta fase

Informação 1: STEP_LAYOUT É usada no início da fase 2 e definida no final STEP_ANIMATIONS. Como será chamada várias vezes, STEP_ANIMATIONStambém é um dos julgamentos iniciais

Informação 2: Diferente da chamada na fase de medida onLayoutChildren, é definida neste momento mState.mInPreLayout = false, por isso é chamada de fase de layout real

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

A seguir , mLayout é tomado LinearLayoutManagercomo exemplo. Primeiro, detachAndScrapAttachedViewstodas as visualizações anexadas existentes em RV são desvinculadas e descartadas. É claro que esse descarte é temporário (consulte Fillo capítulo para obter detalhes) e o objetivo é evitar o impacto de pré- instantâneos durante a fase de pré-layout e prossiga para restaurar a tela. Em seguida, ele é preenchido e o isPreLayoutfill é trazido aqui , o que será mencionado mais adiante.

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

expediçãoLayoutStepSummary

A partir disso, sabemos aproximadamente o significado e a função de cada estágio, e a marca de status é usada para processar o fluxo do estágio (observe que o estágio de medição também será chamado)

evento de slides

Durante o uso do RV, o deslizamento é o mais frequente e o ACTION_MOVE relacionado ao deslizamento não pode ser liberado. Entre eles scrollByInternalestá a lógica específica de seu próprio consumo deslizante. Com a cadeia de chamadas de código, finalmente chegou ao meio LinearLayoutManager.scrollBy, e há mais novas descobertas, que na verdade são chamadas fill(o preenchimento específico de RV quando falamos sobre layoutChildren anteriormente).

Você pode imaginar o seguinte: no processo de deslizar o dedo para cima, o item de cima é gradualmente removido e o novo item de baixo é adicionado. fillDeve haver dois 回收、复用links envolvidos, por isso o separamos em capítulos

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

Preencher reciclagem-reutilização

Esta será a principal prioridade na explicação do RV. É claro que nem todos os LayoutManagers possuem esse método, mas o LinearLayoutManager, por exemplo, onLayoutChildrene scrollVerticallyByeventualmente mudou para esse método. E sua fonte deveria ser, ou seja layout, onTouchEvent.ACTION_MOVEos três principais processos e distribuição de eventos

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

Concentre-se principalmente em dois métodos, layoutChunke recycleByLayoutStateo conceito de espaço restante deve ser bem compreendido, como pode ser visto na ordem dos métodos, primeiro preencher e depois remover. Você pode pensar por que ele não é removido primeiro e depois preenchido?

Supondo que o preenchimento seja baseado na reutilização e a remoção seja baseada na reciclagem, se for removido primeiro e depois preenchido, então o cache que eu esperava que fosse reutilizado primeiro pode ser substituído pela visualização que é removida e adicionada ao cache primeiro, então reciclagem-reutilização não é tão eficiente

esconderijo

回收-复用Este tópico geralmente é acompanhado por 缓存este conceito, como cache ativo do Glide, cache de memória, cache de disco, cache de três níveis , mensagem , tipo de mídia. O cache multinível visa melhorar a eficiência da multiplexação e reduzir o custo de reciclagem

A estrutura de dados do cache usado pelo RV está estragada e é orientada a resultados para evitar confusão no texto de acompanhamento. Claro, o conceito será reforçado novamente a seguir. Os objetos armazenados em cache sãoViewHolder

Existem opiniões diferentes sobre se o cache de terceiro nível ou o cache de quarto nível é, porque mCachedViews na nota oficial é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;
    }
}

Ao entrar oficialmente na reciclagem-reutilização, pense nisso

A prioridade do cache significa que o desempenho de multiplexação correspondente também varia de alto para baixo? (melhor desempenho de multiplexação significa operações menos dispendiosas)

1. Pior caso: recriar ViewHodlere religar dados

2. Segundo melhor caso: reutilizar, ViewHoldermas religar dados

3. Melhor caso: reutilizar ViewHoldersem religar os dados

Citação de - Autor: Tang Zixuan

Reciclar

Quanto à reciclagem, ela deve ser vista em duas partes

1. Conforme mencionado no artigo anterior, “Haverá um pré-layout para poluir a tela para obter um instantâneo de antemão”, fillportanto uma aboliçãodetachAndScrapAttachedViews(recycler) ( onLayoutChildrenin) temporária foi realizada antes . Dois métodos de reciclagem foram encontrados ,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. Comportamento de reciclagemfill que acompanha recycleByLayoutStatea remoção durante o enchimento . Seguindo a cadeia de chamadas , finalmente encontreirecycleByLayoutState >>> recycleViewsFromEnd(Start也是一样) >>> recycleChildren >>> RecyclerView.removeAndRecycleViewAt >>> recycleViewrecycleViewHolderInternal

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

Para entender o limite, você pode consultar esta imagem no artigo de Hongyang, eu a carreguei diretamente aqui

3. Como recycleViewHolderInternalsão duas chamadas, vamos falar primeiro sobre as regras antigas scrapView. Descobrimos que até mesmo o Scrap distinguiu entre mAttached e mChanged e vinculou ViewHolder ao mesmo tempomScrapContainer

Por exemplo, remover deve ir para mAttached, mas alterar é atualizar ( mAdapterHelper.preProcess() mencionado em dispatchLayoutStep1 acima , que lida com as alterações de estado armazenadas em mPendingUpdates e o estado é monitorado e enviado por mObserver:RecyclerViewDataObserver ) está em mChanged.

A partir disso: sabemos que as funções dos dois caches de nível 1 de sucata são consistentes, mas precisamos distinguir se os dados estão poluídos (se precisarem ser atualizados, serão considerados poluídos)

// 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 É semelhante ao nosso conceito de reciclagem diária. Se isRecyclablefor definido falso, não será reciclado, o que afetará seriamente o desempenho, o que significa que novas criações serão criadas a cada vez. Portanto, para o possível distúrbio de renderização de dados causado pela reciclagem e reutilização (não problema do RV, mas problema seu), deve ser por meio do comportamento de renderização correspondente if-elsedas condições em onBindViewHolder .

Preste atenção às principais condições: !holder.hasAnyOfTheFlags, ou seja, o cache nos mCachedViews de segundo nível deve ser utilizável diretamente, sem religação. Ao mesmo tempo, quando mCachedViews estourar, o item mais antigo será recycleCachedViewAtreciclado mRecyclerPool, ou seja, o recurso de fila primeiro a entrar, primeiro a sair, portanto pode-se considerar que mCachedViews pertence a mRecyclerPool 预备队列e seu cache é livre de poluição

// 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 {
         // ……
    }
    // ……
}

Existem aproximadamente tantos conteúdos recuperados e, em retrospecto, parece inútil mViewCacheExtension! ! ! Embora seja testado e recuperado durante a reutilização, não está envolvido no processo de reciclagem, ou seja, é totalmente mantido pelo desenvolvedor (ah, não ~).

Então também entendo duas distinções:

  1. Descartar sucata e cache
  2. Poluição

A seguir será explicado por que o descarte é temporário do ponto de vista da reutilização

reuso

Concluído em 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;
    }
}

A reutilização vem do preenchimento de necessidades, layoutChunkpasso a passo ( layoutState.next >>> recycler.getViewForPosition >>> tryGetViewHolderForPositionByDeadline ), encontramos tryGetViewHolderForPositionByDeadline. Um total de caches foram obtidos aqui 5次尝试e também há dois métodos de adaptador com os quais estamos familiarizados createViewHolder>>>onCreateViewHolder,tryBindViewHolderByDeadline>>>onBindViewHolder

  1. pré-layout
  2. Tome de acordo com a posição
  3. Pegue de acordo com Id e ViewType
  4. cache personalizado
  5. Do buffer pool de acordo com ViewType
  6. Criar ViewHolder

Tendo em vista a extensão do artigo, a lógica interna de cada método pode ser lida em profundidade, portanto não será ampliada uma a uma.

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

Em 2. getScrapOrHiddenOrCachedHolderForPosition , a validade de position e Invalid é verificada . Ao mesmo tempo, mAttachedScrap e mCachedViews também são verificados quanto a não atualização durante a reciclagem, portanto, pode-se inferir que não há necessidade de religar após serem retirados aqui. (Observações: mHiddenViews não está relacionado neste artigo, está relacionado à animação)

Em seguida, combinado com a estratégia de reciclagem, especula-se que mAttachedScrap é usado para recuperação rápida após desanexação (depois remova-o, para onde foi no final? Na verdade, removeAndRecycleScrapInt realizou a reciclagem final em dispatchLayoutStep3 e irá para o pool de cache) , e mCachedViews é usado para Ao deslizar para cima e para baixo repetidamente, como [1, 2, 3], ao deslizar para cima [2, 3, 4] e depois deslizar para baixo [1, 2, 3], multiplexação direta

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

Para 1.getChangedScrapViewForPosition , por que ele é usado apenas no pré-layout? Como alterado neste momento não significa que a religação seja necessária, significa apenas que será atualizado posteriormente, o que é usado para representar um estado de "pré-evento" e, no estágio de layout real, o item que precisa ser ser alterado irá reutilizar o cache no recyclerPool, ou seja, pegar um. O novo ViewHolder, com religação e layout real medido, representa um estado "após o fato". Sendo esse o caso, por que não colocá-lo em mAttachedScrap? Porque na fase de layout real, o mAttachedScrap restante que não foi disposto será onLayoutChildrenchamado no final layoutForPredictiveAnimationspara fazer o layout de todos os restantes, vou dar um exemplo para entender o propósito. Por exemplo, a tela pode acomodar apenas dois itens, [1,2], e a inserção precisa estar entre 1 e 2, então [1, novo2, antigo2] é a aparência real do instantâneo "pré-evento", e mChangedScrap evita que nesta fase seja preenchido

Este parágrafo pode ser um pouco complicado. É recomendável lê-lo várias vezes. Não tenho certeza se esse entendimento está correto. Bem-vindo a me corrigir

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

E 3. Qual é o significado de getScrapOrCachedViewForId ? hasStableIdsO uso de é um complemento para getChangedScrapViewForPosition . Ao mesmo tempo, notifyDataSetChangedverifica-se na cadeia de chamadas de observação que hasStableIds pode evitar a reciclagem neste cenário mCachedViews(o cache está poluído neste momento, o que é muito especial e precisa de atenção, ou seja, é necessária a religação para reutilização). Em alguns cenários específicos, melhora até certo ponto a eficácia e a eficiência da multiplexação

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

pergunta da alma

1. Quando usar a expansão do cache RV

As sugestões a seguir são baseadas em algumas de minha autoria após a leitura do código-fonte 猜想e não foram testadas para obter o efeito de otimização. Bem-vindo ao comunicar. Como usá-lo precisa ser avaliado em combinação com cenários e interações reais

Tomemos como exemplo a coluna de classificação de segundo nível da Classificação Hema. Ao mudar a classificação de primeiro nível, é necessário atualizar e renderizar todas as classificações de segundo nível. Todas elas são consideradas visualizadores poluentes. Como o mesmo ViewType , apenas recyclerPool5 são armazenados em cache por padrão e a figura está próxima de 10. Por um lado, não é suficiente. Com base em dados suficientes, quase todas as trocas requerem createcerca de 5 espectadores. Em seguida, expanda o cache deste tipo ( setMaxRecycledViews), então apenas será necessário religar

Então, mCachedViewsquando é recomendado expandir a capacidade, eu pessoalmente adivinhei, talvez como cachoeiras e grades, pode haver vários que são removidos ao mesmo tempo quando você desliza para cima em uma linha, e então você pode restaurá-lo rapidamente deslizando para baixo uma linha (como grades, grades, etc.) é definida como 2+spanCount por padrão) para evitar ser parcialmente reciclada no recyclerPool para religação devido ao estouro

2. Em que circunstâncias o cache obtido precisa do onBind e quando não?

Mencionamos anteriormente que aqueles RecycledViewPoolretirados precisam ser resetInternalreiniciados e a religação deve ser realizada. E mChangedScrapconfirme que a atualização é necessária para ser diferenciada, portanto, a religação também é necessária

O resto mAttachedScrape a soma mCachedViewssão considerados não incluídos FLAG_UPDATE, portanto, naturalmente, não são necessários. Claro, esta também é a sua limitação, porque só pode ser usado para multiplexação na mesma posição

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

3. Algumas outras sugestões de otimização para RV

  1. Se o tamanho do RV for fixo, pode-se RecyclerView.setHasFixedSize(true)evitar consumo desnecessário requestLayoutde desempenho por
// RecyclerViewDataObserver.java
void triggerUpdateProcessor() {
    if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
        RecyclerView.this.postOnAnimation(mUpdateChildViewsRunnable);
    } else {
        mAdapterUpdateDuringMeasure = true;
        requestLayout();
    }
}
  1. Remova a animação padrão (dependendo da situação)
  2. onBindViewHolder vincula apenas o máximo possível para reduzir o processamento de dados
  3. Use carga útil para atualizar parcialmente, reduza outras ligações de dados onBindViewHolder desnecessárias, pode ser combinado com DiffUtil
  4. Monitore o deslizamento e cancele operações como carregamento de imagem ao deslizar, comoGlide.with(mContext).pauseRequests()

4. Não recomendado para notifyDataSetChanged

Chamar markKnownViewsInvalido método com todas ViewHolderas marcas FLAG_INVALIDsignifica desanexar o link e todos os itens serão reciclados no recyclerViewPool.Se o pool transbordar, ele precisa ser criado para causar consumo no preenchimento

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

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

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

Notas de estudo do Android

Otimização de desempenho do Android: https://qr18.cn/FVlo89
Princípios subjacentes do Android Framework: https://qr18.cn/AQpN4J
Bucket da família Jetpack (incluindo Compose): https://qr18.cn/A0gajp
Carro Android: https://qr18.cn/F05ZCM
Notas de estudo de segurança reversa do Android: https://qr18.cn/CQ5TcL
Áudio e vídeo do Android: https://qr18.cn/Ei3VPD
Kotlin: https://qr18.cn/CdjtAF
Gradle: https://qr18.cn/DzrmMB
Notas de análise de código-fonte OkHttp: https://qr18.cn/Cw0pBD
Flutter: https://qr18.cn/DIvKma
Android Eight Corpo de conhecimento: https://qr18.cn/CyxarU
Android Core Notas: https://qr21.cn/CaZQLo
Perguntas anteriores da entrevista sobre Android: https://qr18.cn/CKV8OZ
Coleção mais recente de perguntas da entrevista sobre Android de 2023: https://qr18.cn/CgxrRy
Exercícios de entrevista de trabalho para desenvolvimento de veículos Android: https://qr18.cn/FTlyCJ
Perguntas da entrevista de áudio e vídeo:https://qr18.cn/AcV6Ap

Acho que você gosta

Origin blog.csdn.net/weixin_61845324/article/details/132602220
Recomendado
Clasificación