RecyclerView Análisis de reciclaje y reutilización

Autor: Cálculo_Xiao Wang

Este artículo comienza con los tres procesos principales y la distribución de eventos de ViewTraversals, combina uso y experiencia y se centra en analizar el mecanismo de reciclaje y reutilización de RecyclerView. Todo el artículo tomará LinearLayoutManager como ejemplo, girará en torno a varios métodos clásicos de reescritura diaria de RecyclerView.Adapter y explicará el mecanismo de almacenamiento en caché de RV.

Este artículo es bastante largo, por lo que te sugiero que lo leas más tarde si olvidas algo y luego leas el artículo anterior.

Tres etapas: medir, diseñar, dibujar.

RV (RecyclerView, al que nos referiremos en el siguiente texto) como ViewGroup, debemos comprender sus tres etapas principales. Entre ellos, nos centraremos en el diseño (de eso hablaremos al final)

medida

En la fase de medición, la atención se centra principalmente mLayout.isAutoMeasureEnabled()en el segmento de código, porque el LayoutManager habitual está habilitado de forma predeterminada. Para medir ViewGroup, necesitamos saber el tamaño del niño antes de poder decidir, especialmente para RV. Por ejemplo LinearLayoutManger.height = WRAP_CONTENT, el tamaño específico debe conocerse después de colocar al niño. setMeasuredDimensionFromChildrenFinalmente llamado setMeasuredDimension, también significa que dispatchLayoutStep2es muy probable que determine la medida y el diseño del elemento secundario.

// 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, sin demasiada elaboración, debemos prestar atención al dispatchLayoutStep2desempeño específico en el seguimiento. Y podemos inferir que el tamaño fijo puede acelerar efectivamente la fase de medición. Por supuesto, en algunas anidaciones de listas grandes, puede ser inevitable

dibujar

Esta es la única entre las tres fases principales de RV que tiene un comportamiento de iniciación de reescritura activa (diferente del comportamiento de respuesta de onXX). De esto, aprendimos que ItemDecorationsel dibujo de también está involucrado en esta etapa, y el orden de onDraw y onDrawOver también es claro. Por supuesto, la configuración de compensación de ItemDecorations debe estar en layoutla etapa

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

De las dos fases de medida y diseño, y del uso básico de RV, nos damos cuenta de que RV divide diferentes responsabilidades en diferentes objetos (LayoutManager, ItemDecoration, Adapter, cuyos nombres son apropiados al significado, por lo que no explicaré mucho). y luego la intervención dirigida en el enlace principal es una excelente práctica de desacoplamiento

disposición

Hay dispatchLayouttres métodos con casi el mismo nombre: despachoLayoutStep1\2\3, y luego hay llamadas correspondientes en la fase Medir, que también tiene un indicador de estado mState.mLayoutStep, luego continuamos con preguntas:

  1. ¿Cuáles son las funciones y el significado de los tres métodos?
  2. Cómo cambian las banderas de estado y qué hacen
// 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();
}

De manera similar, ajustamos el orden de lectura para que sea 1, 3 y finalmente 2.

despachoDiseñoPaso1

Información 1: STEP_START Se utiliza al comienzo de la fase 1 y se establece enSTEP_LAYOUT

Información 2: la información principal extraída parece estar Animationrelacionada con mRunSimpleAnimations y mRunPredictiveAnimations , y los dos juicios importantes se processAdapterUpdatesAndSetAnimationFlags()procesan en ellos, echemos un vistazo más profundo

Información 3: Llamado una vez onLayoutChildren, los detalles específicos se publican en despachoLayoutStep2 , ahora solo necesita saber que se ha realizado un diseño, generalmente lo llamamos预布局

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

Hay muchos juicios y procesamientos en esta área. En resumen: RV admite animación y mItemsAddedOrRemoved || mItemsChangedes cierto. también mRunPredictiveAnimationsdepende demRunSimpleAnimations

Punto clave: mAdapterHelper.preProcess() es un método que es casi necesario (la escena especial de eliminar animación no se considera por el momento) y maneja los cambios del adaptador (como agregar\eliminar\cambiar), que registra el comportamiento específico. del cambio, y la cadena de llamadas se puede explorar por sí misma (no se hace mPendingUpdatesaquí Expandir), en RecyclerView mObserver:RecyclerViewDataObserver, es decir, cuando se llama a adaptor.notifyXX (cualquier método de actualización), responderá y se agregará el comportamiento correspondiente 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();
}

En términos generales, la etapa 1 conduce al 预布局concepto, y el diseño previo y la visualización de la animación (agregar\eliminar\cambiar) son extremadamente relevantes.

唐子玄El significado de una oración explicada en el artículo citado por el hermano mayor 预布局es simplemente dejar una instantánea antes de la animación para compararla y determinar cómo se ejecutará la animación.

Hay dos elementos (1, 2) en la lista, elimine 2, ahora 3 se moverá suavemente desde la parte inferior de la pantalla y ocupará la posición original de 2. Para lograr este efecto, la estrategia de RecyclerView es: primero realizar un diseño previo para el elemento de la tabla antes de la animación y cargar el elemento 3 de la tabla invisible en el diseño para formar una instantánea del diseño (1, 2, 3). Luego, realice un diseño posterior para el elemento de la tabla después de la animación y también forme una instantánea del diseño (1, 3). Compara la posición de la entrada 3 en las dos instantáneas y sabrás cómo animarla.

despachoDiseñoPaso3

Mensaje 1: STEP_ANIMATIONS Se utiliza al inicio de la fase 3 y no es necesario porque vuelve inmediatamente aSTEP_START

Mensaje 2: mViewInfoStore.process La animación finalmente se activa en el medio. Este artículo no ampliará los detalles de la animación, que generalmente se denomina posdiseño.postLayout

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

despachoDiseñoPaso2

Esta es la única de las tres fases que se puede llamar varias veces y también establece el estado final de las vistas durante esta fase.

Información 1: STEP_LAYOUT se usa al comienzo de la fase 2 y se establece al final STEP_ANIMATIONS. Dado que se llamará varias veces, STEP_ANIMATIONStambién es uno de los juicios iniciales.

Información 2: A diferencia de la llamada en la fase de medición onLayoutChildren, se configura en este momento mState.mInPreLayout = false, por lo que se llama fase de diseño 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 continuación , se toma mLayoutLinearLayoutManager como ejemplo. Primero, detachAndScrapAttachedViewstodas las vistas adjuntas existentes en RV se desatan y se descartan. Por supuesto, este descarte es temporal (consulte Fillel capítulo para obtener más detalles) y el propósito es evitar el impacto de la pre- instantáneas durante la etapa de prediseño y proceda a restaurar el lienzo. Luego se llena y fillse trae aquí isPreLayout , que se mencionará más adelante.

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

despachoDiseñoPasoResumen

A partir de esto, conocemos aproximadamente el significado y la función de cada etapa, y la marca de estado se usa para procesar el flujo de la etapa (tenga en cuenta que también se llamará etapa de medición)

evento de diapositiva

Durante el uso de RV, el deslizamiento es lo más frecuente y no se puede dejar de lado ACTION_MOVE relacionado con el deslizamiento. Entre ellos scrollByInternalestá la lógica específica de su propio consumo deslizante.Con la cadena de llamadas de código, finalmente llegó al medio LinearLayoutManager.scrollBy, y hay más descubrimientos nuevos, que en realidad se llaman fill(el relleno RV específico cuando hablamos de layoutChildren antes).

Puedes imaginar esto: en el proceso de deslizar el dedo hacia arriba, el elemento superior se elimina gradualmente y se agrega el nuevo elemento en la parte inferior. fillDebe haber dos 回收、复用enlaces involucrados, por eso lo separamos en 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);
    // ……
}

Llenar reciclaje-reutilización

Esta será la máxima prioridad en la explicación de RV. Por supuesto, no todos los LayoutManagers tienen este método, pero LinearLayoutManager, por ejemplo, onLayoutChildrenfinalmente scrollVerticallyByoptó por este método. Y su fuente debería ser, es decir layout, onTouchEvent.ACTION_MOVElos tres procesos principales y la distribución 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;
}

Concéntrese principalmente en dos métodos, layoutChunky recycleByLayoutStateel concepto de espacio restante debe comprenderse bien, como se puede ver en el orden de los métodos, primero llenar y luego eliminar. ¿Puedes pensar por qué no se quita primero y luego se llena?

Suponiendo que el llenado se basa en la reutilización y la eliminación se basa en el reciclaje, si se elimina primero y luego se llena, entonces el caché que esperaba reutilizar primero puede ser reemplazado por la vista que se elimina y se agrega primero al caché, luego Reciclar-reutilizar no es tan eficiente

cache

回收-复用Este tema suele ir acompañado de 缓存este concepto, como caché activo de Glide, caché de memoria, caché de disco de tres niveles , mensaje , tipo de medio. El caché multinivel tiene como objetivo mejorar la eficiencia de la multiplexación y reducir el costo de reciclaje.

La estructura de datos del caché utilizado por RV está estropeada y está orientada a resultados para evitar confusiones en el texto siguiente. Por supuesto, el concepto se reforzará nuevamente a continuación. Los objetos almacenados en caché sonViewHolder

Hay diferentes opiniones sobre si es caché de tercer nivel o caché de cuarto nivel, porque mCachedViews en la nota oficial esfirst-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;
    }
}

Cuando ingrese oficialmente al reciclaje-reutilización, tenga en cuenta esto

¿La prioridad de la caché significa que el rendimiento de multiplexación correspondiente también es de mayor a menor? (un mejor rendimiento de multiplexación significa operaciones menos costosas)

1. Peor de los casos: recrear ViewHodlery volver a vincular datos

2. Segundo mejor caso: reutilizar ViewHolderpero volver a vincular datos

3. Mejor caso: reutilizar ViewHoldersin volver a vincular datos

Cita de - Autor: Tang Zixuan

Reciclar

En cuanto al reciclaje, hay que verlo en dos partes.

1. Como se mencionó en el artículo anterior, "Habrá un diseño previo para contaminar el lienzo para obtener una instantánea de antemano", fillpor lo que ha llevado a cabo una abolicióndetachAndScrapAttachedViews(recycler) ( onLayoutChildrenin) temporal antes . Se encontraron dos métodos de recuperación ,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. Comportamiento de reciclajefill que acompaña recycleByLayoutStatea la eliminación durante el llenado . Siguiendo la cadena de llamadas , finalmente encontrérecycleByLayoutState >>> 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 comprender el límite, puede consultar esta imagen en el artículo de Hongyang, la llevé directamente aquí.

3. Dado que recycleViewHolderInternalhay dos llamadas, hablemos primero de las reglas antiguas scrapView. Descubrimos que incluso Scrap ha distinguido entre mAttached y mChanged, y ha vinculado ViewHolder al mismo tiempo.mScrapContainer

Por ejemplo, eliminar es ir a mAttached, pero cambiar es actualizar ( mAdapterHelper.preProcess() mencionado en despachoLayoutStep1 anteriormente maneja los cambios de estado almacenados en mPendingUpdates , y el estado es monitoreado y enviado por mObserver:RecyclerViewDataObserver ) a mChanged.

De esto: sabemos que las funciones de los dos cachés de nivel 1 de desechos son consistentes, pero debemos distinguir si los datos están contaminados (si es necesario actualizarlos, se consideran contaminados)

// 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 Es similar a nuestro concepto de reciclaje diario: si isRecyclablese establece falso, no se reciclará, lo que afectará seriamente el rendimiento, lo que significa que se crearán nuevas creaciones cada vez. if-elsePor lo tanto, para el posible desorden de representación de datos causado por el reciclaje y la reutilización (no es un problema de RV, sino su problema), debe ser a través del comportamiento de representación correspondiente de las condiciones en onBindViewHolder .

Preste atención a las condiciones clave: !holder.hasAnyOfTheFlags, es decir, el caché en mCachedViews de segundo nivel debe poder usarse directamente sin volver a vincularse. Al mismo tiempo, cuando mCachedViews se desborda, se reciclará el elemento más antiguo recycleCachedViewAt, mRecyclerPooles decir, la función de cola de primero en entrar, primero en salir, por lo que se puede considerar que mCachedViews pertenece a mRecyclerPool 预备队列y su caché está libre de contaminación.

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

¡Hay tantos contenidos recuperados que, en retrospectiva, parece inútil mViewCacheExtension! ! ! Aunque se probará y recuperará durante la reutilización, no participa en el proceso de reciclaje, es decir, el desarrollador lo mantiene completamente (oh no ~).

Entonces también entiendo dos distinciones:

  1. Descartar chatarra y caché
  2. Contaminación

A continuación se explicará por qué el descarte es temporal desde la perspectiva de la reutilización.

reutilizar

Completado en diseñoChunk

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

La reutilización proviene de satisfacer las necesidades, layoutChunkpaso a paso ( layoutState.next >>> recycler.getViewForPosition >>> tryGetViewHolderForPositionByDeadline ), lo encontramos tryGetViewHolderForPositionByDeadline. Aquí se han tomado un total de cachés 5次尝试y también hay dos métodos de adaptador con los que estamos familiarizados createViewHolder>>>onCreateViewHolder.tryBindViewHolderByDeadline>>>onBindViewHolder

  1. pre-diseño
  2. Tomar según posición
  3. Tomar según Id y ViewType
  4. caché personalizado
  5. Obtener del grupo de búfer según ViewType
  6. Crear soporte de vista

Dada la extensión del artículo, la lógica interna de cada método se puede leer en profundidad, por lo que no se ampliará uno por uno.

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

En 2. getScrapOrHiddenOrCachedHolderForPosition , se verifica la validez de la posición y Invalid . Al mismo tiempo, mAttachedScrap y mCachedViews también se verifican para ver si no están actualizados durante el reciclaje, por lo que se puede inferir que no es necesario volver a vincularlos después de retirarlos. aquí. (Observaciones: mHiddenViews no se trata en este artículo, está relacionado con la animación)

Luego, combinado con la estrategia de reciclaje, se especula que mAttachedScrap se usa para una recuperación rápida después de la separación (luego elimínelo, ¿a dónde fue al final? De hecho, removeAndRecycleScrapInt realizó el reciclaje final en despachoLayoutStep3 e irá al grupo de caché) Y mCachedViews se usa para Cuando se desliza hacia arriba y hacia abajo repetidamente, como [1, 2, 3], cuando se desliza hacia arriba [2, 3, 4] y luego se desliza hacia abajo [1, 2, 3], multiplexación directa

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 qué solo se usa en el diseño previo? Debido a que cambiar en este momento no significa que sea necesario volver a vincularlo, solo significa que se actualizará más adelante, lo que se utiliza para representar un estado "previo al evento", y en la etapa de diseño real, el elemento que necesita cambiar reutilizará el caché en recyclerPool, es decir, tomará uno. El nuevo ViewHolder, con reenlace y diseño medido real, representa un estado "posterior al hecho". Siendo ese el caso, ¿por qué no ponerlo en mAttachedScrap? Porque en la etapa de diseño real, el mAttachedScrap restante que no se ha diseñado se onLayoutChildrenllamará al final layoutForPredictiveAnimationspara diseñar todos los restantes, les daré un ejemplo para que comprendan el propósito. Por ejemplo, la pantalla solo puede contener dos elementos, [1,2], y el inserto debe estar entre 1 y 2, entonces [1, nuevo2, antiguo2] es la apariencia real de la instantánea "anterior al evento", y mChangedScrap evita que en esta etapa se rellene

Este párrafo puede ser un poco complicado. Se recomienda leerlo varias veces. No estoy seguro de si esta comprensión es correcta. Bienvenido a corregirme.

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

Y 3. ¿Cuál es el significado de getScrapOrCachedViewForId ? hasStableIdsEl uso de es un complemento de getChangedScrapViewForPosition . Al mismo tiempo, notifyDataSetChangedse encuentra en la cadena de llamadas de observación que hasStableIds puede evitar el reciclaje en este escenario mCachedViews(el caché está contaminado en este momento, lo cual es muy especial y necesita atención, es decir, se requiere volver a vincularlo para reutilizarlo). En algunos escenarios específicos, mejora la efectividad y eficiencia de la multiplexación hasta cierto punto.

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

pregunta del alma

1. Cuándo utilizar la expansión de caché RV

Las siguientes sugerencias se basan en algunas de las mías después de leer el código fuente 猜想y no han sido probadas para obtener el efecto de optimización. Bienvenido a comunicarse. La forma de utilizarlo debe evaluarse en combinación con escenarios e interacciones reales.

Tome la columna de clasificación de segundo nivel de la Clasificación Hema como ejemplo. Al cambiar la clasificación de primer nivel, es necesario actualizar y renderizar todas las clasificaciones de segundo nivel. Todas ellas se consideran visualizadores contaminantes. Como el mismo ViewType , solo recyclerPool5 se almacenan en caché de forma predeterminada, y en la figura está cerca de 10. Por un lado, no es suficiente. Según datos suficientes, casi todos los cambios requieren createalrededor de cinco espectadores. Luego expanda el caché de este tipo ( setMaxRecycledViews), luego solo se requiere volver a vincular

Entonces, ¿ mCachedViewscuándo se recomienda expandir la capacidad? Personalmente lo adiviné, tal vez como cascadas y cuadrículas, puede haber varias que se eliminan al mismo tiempo cuando se desliza hacia arriba en una fila, y luego puede restaurarlas rápidamente deslizándose hacia abajo. una fila (como cuadrículas, cuadrículas, etc.) está configurada en 2+spanCount de forma predeterminada) para evitar que se recicle parcialmente en recyclerPool para volver a vincularse debido a un desbordamiento

2. ¿Bajo qué circunstancias la caché recuperada necesita onBind y cuándo no?

Mencionamos anteriormente que los que RecycledViewPoolse deben resetInternalrestablecerse y se debe volver a vincular. Y mChangedScrapconfirme que es necesario distinguir la actualización, por lo que también es necesario volver a vincular

Se considera que el resto mAttachedScrapy la suma mCachedViewsno están incluidos FLAG_UPDATE, por lo que, naturalmente, no son necesarios. Por supuesto, esta también es su limitación, porque solo se puede utilizar para multiplexar en la misma posición.

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

3. Algunas otras sugerencias de optimización para vehículos recreativos

  1. Si el tamaño del RV es fijo, se puede RecyclerView.setHasFixedSize(true)evitar requestLayoutun consumo innecesario de rendimiento al
// RecyclerViewDataObserver.java
void triggerUpdateProcessor() {
    if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
        RecyclerView.this.postOnAnimation(mUpdateChildViewsRunnable);
    } else {
        mAdapterUpdateDuringMeasure = true;
        requestLayout();
    }
}
  1. Eliminar la animación predeterminada (según la situación)
  2. onBindViewHolder solo vincula tanto como sea posible para reducir el procesamiento de datos
  3. Utilice la carga útil para actualizar parcialmente y reducir otros enlaces de datos innecesarios de onBindViewHolder; se puede combinar con DiffUtil
  4. Supervise el deslizamiento y cancele operaciones como la carga de imágenes al deslizarse, comoGlide.with(mContext).pauseRequests()

4. No recomendado para notificarDataSetChanged

Llamar markKnownViewsInvalidal método con todas ViewHolderlas marcas FLAG_INVALIDsignifica desconectar el enlace y todos los elementos se reciclarán en el recyclerViewPool. Si el grupo se desborda, es necesario crearlo para provocar consumo al llenarlo.

@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 estudio de Android

Optimización del rendimiento de Android: https://qr18.cn/FVlo89
Principios subyacentes del marco de trabajo de Android: https://qr18.cn/AQpN4J
Grupo de la familia Jetpack (incluido Compose): https://qr18.cn/A0gajp
Auto Android: https://qr18.cn/F05ZCM
Notas de estudio de seguridad inversa de Android: https://qr18.cn/CQ5TcL
Audio y video de Android: https://qr18.cn/Ei3VPD
Kotlin: https://qr18.cn/CdjtAF
Gradle: https://qr18.cn/DzrmMB
Notas de análisis del código fuente OkHttp: https://qr18.cn/Cw0pBD
Flutter: https://qr18.cn/DIvKma
Android Eight Knowledge Body: https://qr18.cn/CyxarU
Android Core Notas: https://qr21.cn/CaZQLo
Preguntas de entrevistas anteriores de Android: https://qr18.cn/CKV8OZ
Última colección de preguntas de entrevistas de Android de 2023: https://qr18.cn/CgxrRy
Ejercicios de entrevistas de trabajo de desarrollo de vehículos Android: https://qr18.cn/FTlyCJ
Preguntas de entrevistas de audio y video:https://qr18.cn/AcV6Ap

Supongo que te gusta

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