RecyclerView Analyse du recyclage et de la réutilisation

Auteur : Calculus_Xiao Wang

Cet article commence par les trois principaux processus et distribution d'événements de ViewTraversals, combine l'utilisation et l'expérience et se concentre sur l'analyse du mécanisme de recyclage et de réutilisation de RecyclerView. L'article entier prendra LinearLayoutManager comme exemple, s'articulera autour de plusieurs méthodes classiques de réécriture quotidienne de RecyclerView.Adapter et expliquera le mécanisme de mise en cache de RV

Cet article est assez long, je vous suggère donc de le lire plus tard si vous oubliez quelque chose, puis de lire l'article précédent

Trois étapes : mesurer, tracer, dessiner

RV (RecyclerView, mentionné dans le texte suivant) en tant que ViewGroup, nous devons comprendre ses trois grandes étapes. Parmi eux, nous nous concentrerons sur la mise en page (nous en reparlerons donc à la fin)

mesure

Dans la phase de mesure, l'accent est mis mLayout.isAutoMeasureEnabled()sur le segment de code, car le LayoutManager habituel est activé par défaut. Pour mesurer ViewGroup, nous devons connaître la taille de l'enfant avant de pouvoir décider, en particulier pour RV. Par exemple LinearLayoutManger.height = WRAP_CONTENT, la taille spécifique doit être connue après le placement de l'enfant. setMeasuredDimensionFromChildrenEnfin appelé setMeasuredDimension, cela signifie également qu'il dispatchLayoutStep2est très probable qu'il détermine la mesure && la disposition de l'élément enfant

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

Pour cette étape, sans trop d’élaboration, nous devons prêter attention aux dispatchLayoutStep2performances spécifiques dans le suivi. Et nous pouvons en déduire que la taille fixe peut effectivement accélérer la phase de mesure. Bien sûr, dans certaines imbrications de grandes listes, cela peut être inévitable

dessiner

C'est la seule parmi les trois phases majeures de RV qui a un comportement d'initiation de réécriture actif (différent du comportement de réponse de onXX). De là, nous avons appris que ItemDecorationsle dessin de est également impliqué dans cette étape, et l'ordre de ses onDraw et onDrawOver est également clair. Bien entendu, le paramètre de décalage de ItemDecorations doit être au layoutstade

// 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 des deux phases de mesure et de mise en page, et de l'utilisation de base de RV, on se rend compte que RV répartit différentes responsabilités entre différents objets (LayoutManager, ItemDecoration, Adapter, dont les noms sont appropriés à la signification, donc je n'expliquerai pas grand chose), puis une intervention ciblée sur le maillon principal est une excellente pratique de découplage

mise en page

Il dispatchLayoutexiste trois méthodes portant presque le même nom : dispatchLayoutStep1\2\3, puis il y a les appels correspondants dans la phase Measure, qui a également un indicateur d'état mState.mLayoutStep, puis nous continuons avec les questions :

  1. Quelles sont les fonctions et la signification des trois méthodes
  2. Comment les indicateurs de statut changent et ce qu'ils font
// 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 même, nous ajustons l'ordre de lecture pour qu'il soit 1, 3 et enfin 2

dispatchLayoutStep1

Info 1 : STEP_START Utilisé au début de la phase 1 et réglé surSTEP_LAYOUT

Informations 2 : les principales informations extraites semblent être Animationliées à, et les deux jugements importants mRunSimpleAnimations et mRunPredictiveAnimations sont tous processAdapterUpdatesAndSetAnimationFlags()traités, examinons de plus près

Information 3 : Appelée une fois onLayoutChildren, les détails spécifiques sont publiés dans dispatchLayoutStep2 , il suffit maintenant de savoir qu'une mise en page a été réalisée, nous l'appelons généralement预布局

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

Il y a beaucoup de jugements et de traitements dans ce domaine. En résumé : RV prend en charge l'animation et mItemsAddedOrRemoved || mItemsChangedest vrai. mRunPredictiveAnimationscela dépend aussi demRunSimpleAnimations

Point clé : mAdapterHelper.preProcess() c'est une méthode presque nécessaire (la scène spéciale de suppression de l'animation n'est pas prise en compte pour le moment), et elle gère les modifications de l'adaptateur (comme add\remove\change), qui enregistre le comportement spécifique du changement, et la chaîne d'appels peut être explorée par elle-même (pas fait mPendingUpdatesici Expand), dans RecyclerView mObserver:RecyclerViewDataObserver, c'est-à-dire que lorsque adapter.notifyXX est appelé (n'importe quelle méthode d'actualisation), il répondra et le comportement correspondant sera ajouté à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();
}

D'une manière générale, l'étape 1 mène au 预布局concept, et la pré-mise en page et l'affichage de l'animation (ajouter\supprimer\changer) sont extrêmement pertinents.

唐子玄Le sens d' une phrase expliquée dans l'article cité par le grand frère 预布局est simplement de laisser un instantané avant l'animation pour comparaison afin de déterminer comment l'animation sera exécutée

Il y a deux éléments (1, 2) dans la liste, supprimez 2, maintenant 3 se déplacera en douceur depuis le bas de l'écran et occupera la position d'origine du 2. Afin d'obtenir cet effet, la stratégie de RecyclerView est la suivante : effectuez d'abord une pré-mise en page pour l'élément de tableau avant l'animation, puis chargez l'élément de tableau invisible 3 dans la mise en page pour former un instantané de mise en page (1, 2, 3). Effectuez ensuite une post-mise en page pour l'élément de tableau après l'animation et formez également un instantané de mise en page (1, 3). Comparez la position de l'entrée 3 dans les deux instantanés, et vous saurez comment l'animer.

dispatchLayoutStep3

Message 1 : STEP_ANIMATIONS Utilisé au début de la phase 3, et pas nécessaire, car il revient immédiatement àSTEP_START

Message 2 : mViewInfoStore.process L'animation se déclenche enfin au milieu. Cet article ne s'étendra pas sur les détails de l'animation, généralement appelée post-mise en page.postLayout

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

dispatchLayoutStep2

C'est la seule des trois phases qui peut être appelée plusieurs fois, et elle établit également l'état final des vues au cours de cette phase.

Information 1 : STEP_LAYOUT Elle est utilisée au début de la phase 2, et est fixée à la fin STEP_ANIMATIONS. Puisqu'elle sera appelée plusieurs fois, STEP_ANIMATIONSelle fait également partie des jugements de départ.

Information 2 : Différent de l'appel dans la phase de mesure onLayoutChildren, elle est définie à ce moment-là mState.mInPreLayout = false, on l'appelle donc la phase de mise en page réelle

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

Dans ce qui suit , mLayout est pris LinearLayoutManagercomme exemple. Tout d'abord, detachAndScrapAttachedViewstoutes les vues attachées existantes dans RV sont dissociées et supprimées. Bien entendu, cette suppression est temporaire (voir Fillle chapitre pour plus de détails), et le but est d'éviter l'impact des pré- instantanés pendant la phase de pré-mise en page. Et procédez à la restauration du canevas. Ensuite, il est rempli et isPreLayoutfill est introduit ici , ce qui sera mentionné plus tard.

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

dispatchLayoutStepSummary

À partir de là, nous connaissons à peu près la signification et la fonction de chaque étape, et la marque d'état est utilisée pour le traitement du flux d'étape (notez que l'étape de mesure sera également appelée)

événement de diapositive

Lors de l'utilisation de RV, le glissement est le plus fréquent, et ACTION_MOVE lié au glissement ne peut pas être lâché. Parmi eux scrollByInternalse trouve la logique spécifique de sa propre consommation glissante. Avec la chaîne d'appel de code, il est finalement arrivé au milieu LinearLayoutManager.scrollBy, et il y a plus de nouvelles découvertes, qui sont en fait appelées fill(le remplissage RV spécifique lorsque nous avons parlé de layoutChildren plus tôt).

Vous pouvez imaginer ceci : en faisant glisser votre doigt vers le haut, l'élément du haut est progressivement supprimé et le nouvel élément du bas est ajouté. Il filldoit y avoir 回收、复用deux liens impliqués, c'est pourquoi nous le séparons en chapitres

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

Remplissage recyclage-réutilisation

Ce sera la priorité absolue dans l'explication RV. Bien sûr, tous les LayoutManagers n'ont pas cette méthode, mais le LinearLayoutManager par exemple onLayoutChildrenet scrollVerticallyBya finalement évolué vers cette méthode. Et sa source devrait être, c'est layout- à-dire onTouchEvent.ACTION_MOVEles trois principaux processus et distribution d'événements

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

Concentrez-vous principalement sur deux méthodes, layoutChunket recycleByLayoutState, le concept d'espace restant doit être bien compris, comme le montre l'ordre des méthodes, d'abord remplir puis supprimer. Vous pouvez vous demander pourquoi il n'est pas d'abord retiré puis rempli ?

En supposant que le remplissage est basé sur la réutilisation et que la suppression est basée sur le recyclage, s'il est d'abord supprimé puis rempli, alors le cache que je m'attendais à être réutilisé en premier peut être remplacé par la vue qui est d'abord supprimée et ajoutée au cache, puis le recyclage-réutilisation n'est pas si efficace

cache

回收-复用Ce sujet est généralement accompagné de 缓存ce concept, tel que le cache actif de Glide, le cache mémoire, le cache disque à trois niveaux , Message . Le cache multi-niveaux vise à améliorer l'efficacité du multiplexage et à réduire le coût du recyclage

La structure des données du cache utilisé par RV est gâtée et elle est orientée vers les résultats pour éviter toute confusion dans le texte suivant. Bien entendu, le concept sera à nouveau renforcé ci-dessous. Les objets mis en cache sontViewHolder

Il existe différentes opinions quant à savoir si le cache de troisième niveau ou le cache de quatrième niveau l'est, car mCachedViews dans la note officielle estfirst-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;
    }
}

Lorsque vous vous lancez officiellement dans le recyclage-réutilisation, pensez à cette réflexion

La priorité du cache signifie-t-elle que les performances de multiplexage correspondantes vont également de haut en bas ? (de meilleures performances de multiplexage signifient des opérations moins coûteuses à réaliser)

1. Dans le pire des cas : recréer ViewHodleret relier les données

2. Deuxième meilleur cas : réutiliser ViewHoldermais relier les données

3. Meilleur cas : réutilisation ViewHoldersans relier les données

Citation de - Auteur : Tang Zixuan

Recycler

Concernant le recyclage, il faut l’envisager en deux parties

1. Comme mentionné dans l'article précédent, "Il y aura un pré-layout pour polluer la toile afin d'obtenir un instantané au préalable", filldonc une suppressiondetachAndScrapAttachedViews(recycler) ( onLayoutChildrenin) temporaire a été réalisée auparavant . Deux méthodes de récupération ont été trouvées ,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. Comportement de recyclagefill qui accompagne recycleByLayoutStatele retrait lors du remplissage . En suivant la chaîne d'appels , j'ai finalement trouvé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);
    // ……
}

Pour comprendre la limite, vous pouvez vous référer à cette image dans l'article de Hongyang, je l'ai directement portée ici

3. Puisqu'il recycleViewHolderInternaly a deux appels, parlons d'abord des anciennes règles scrapView. Nous avons constaté que même Scrap faisait la distinction entre mAttached et mChanged, et liait ViewHolder en même temps.mScrapContainer

Par exemple, supprimer consiste à accéder à mAttached, mais le changement est la mise à jour ( mAdapterHelper.preProcess() mentionné dans dispatchLayoutStep1 ci-dessus , qui gère les changements d'état stockés dans mPendingUpdates , et l'état est surveillé et envoyé par mObserver:RecyclerViewDataObserver ) est dans mChanged.

De là : on sait que les fonctions des deux caches scrap niveau 1 sont cohérentes, mais il faut distinguer si les données sont polluées (si elles doivent être mises à jour, elles sont considérées comme polluées)

// 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 C'est similaire à notre concept de recyclage quotidien. Si isRecyclablefalse est défini, il ne sera pas recyclé, ce qui affectera sérieusement les performances, ce qui signifie qu'une nouvelle création sera créée à chaque fois. Par conséquent, pour l'éventuel trouble du rendu des données causé par le recyclage et la réutilisation (pas le problème de RV, mais votre problème), cela devrait se faire via le comportement de rendu correspondant if-elsedes conditions dans onBindViewHolder .

Faites attention aux conditions clés :!holder.hasAnyOfTheFlags , c'est-à-dire que le cache dans les mCachedViews de deuxième niveau doit être directement utilisable sans nouvelle liaison. Dans le même temps, lorsque mCachedViews déborde, l'élément le plus ancien sera recycleCachedViewAtrecyclé mRecyclerPool, c'est-à-dire la fonction de file d'attente premier entré, premier sorti, on peut donc considérer que mCachedViews appartient à mRecyclerPool 预备队列et que son cache est sans pollution

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

Il y a à peu près autant de contenus récupérés, et rétrospectivement, cela semble inutile mViewCacheExtension! ! ! Bien qu'il soit essayé et récupéré lors de sa réutilisation, il n'est pas impliqué dans le processus de recyclage, c'est-à-dire qu'il est entièrement entretenu par le développeur (oh non ~).

Ensuite, je comprends également deux distinctions :

  1. Supprimer les déchets et le cache
  2. Pollution

Ce qui suit expliquera pourquoi le rejet est temporaire du point de vue de la réutilisation.

réutilisation

Terminé dans 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;
    }
}

La réutilisation vient du remplissage des besoins, layoutChunkétape par étape ( layoutState.next >>> recycler.getViewForPosition >>> tryGetViewHolderForPositionByDeadline ), nous l'avons trouvé tryGetViewHolderForPositionByDeadline. Un total de caches ont été pris ici 5次尝试, Et il y a aussi deux méthodes d'adaptation que nous connaissons bien createViewHolder>>>onCreateViewHolder,tryBindViewHolderByDeadline>>>onBindViewHolder

  1. pré-mise en page
  2. Prendre selon la position
  3. Prendre en fonction de l'ID et du ViewType
  4. cache personnalisé
  5. Extraire du pool de tampons selon ViewType
  6. Créer un ViewHolder

Compte tenu de la longueur de l’article, la logique interne de chaque méthode peut être lue en profondeur, elle ne sera donc pas développée une à une.

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

Dans 2. getScrapOrHiddenOrCachedHolderForPosition , la validité de la position et Invalid est vérifiée .En même temps, mAttachedScrap et mCachedViews sont également vérifiés pour la non-mise à jour lors du recyclage, on peut donc en déduire qu'il n'est pas nécessaire de se lier à nouveau après avoir été retirés ici. (Remarques : mHiddenViews n'est pas concerné dans cet article, il est lié à l'animation)

Ensuite, combiné avec la stratégie de recyclage, il est supposé que mAttachedScrap est utilisé pour une récupération rapide après le détachement (puis supprimez-le, où est-il passé à la fin ? En fait, RemoveAndRecycleScrapInt a effectué le recyclage final dans dispatchLayoutStep3 et ira dans le pool de cache) , et mCachedViews est utilisé pour le glissement vers le haut et vers le bas à plusieurs reprises, comme [1, 2, 3], lors du glissement vers le haut [2, 3, 4], puis vers le bas [1, 2, 3], le multiplexage direct

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

Pour 1.getChangedScrapViewForPosition , pourquoi est-il utilisé uniquement en pré-mise en page ? Parce que modifié à ce moment ne signifie pas qu'une nouvelle liaison est requise, cela signifie simplement qu'il sera mis à jour plus tard, ce qui est utilisé pour représenter un état "pré-événement", et dans la phase de mise en page réelle, l'élément qui doit être modifié réutilisera le cache dans le recyclerPool, c'est-à-dire en prendra un. Le nouveau ViewHolder, avec une re-reliure et une disposition réelle mesurée, représente un état "après coup". Cela étant, pourquoi ne pas le mettre dans mAttachedScrap ? Parce que dans la phase de mise en page proprement dite, le mAttachedScrap restant qui n'a pas été disposé sera onLayoutChildrenappelé à la fin layoutForPredictiveAnimationspour disposer tous les autres. Je vais vous donner un exemple pour comprendre le but. Par exemple, l'écran ne peut accueillir que deux éléments, [1,2], et l'insertion doit être comprise entre 1 et 2, alors [1, new2, old2] est l'apparence réelle de l'instantané "pré-événement", et mChangedScrap évite qu'il soit rempli à ce stade

Ce paragraphe est peut-être un peu alambiqué. Il est recommandé de le lire plusieurs fois. Je ne suis pas sûr que cette compréhension soit correcte. Bienvenue pour me corriger

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

Et 3. Quelle est la signification de getScrapOrCachedViewForId ? hasStableIdsL'utilisation de est un complément à getChangedScrapViewForPosition . Dans le même temps, notifyDataSetChangedon constate dans la chaîne d'appels d'observation que hasStableIds peut éviter le recyclage dans ce scénario mCachedViews(le cache est pollué à ce moment-là, ce qui est très particulier et nécessite une attention particulière, c'est-à-dire qu'une nouvelle liaison est nécessaire pour la réutilisation). Dans certains scénarios spécifiques, cela améliore dans une certaine mesure l'efficacité et l'efficience du multiplexage.

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

question d'âme

1. Quand utiliser l'extension du cache RV

Les suggestions suivantes sont basées sur certaines de mes propres suggestions après avoir lu le code source 猜想et n'ont pas été testées pour obtenir l'effet d'optimisation. Bienvenue pour communiquer. La manière de l'utiliser doit être évaluée en combinaison avec des scénarios et des interactions réels.

Prenons l'exemple de la colonne de classification de deuxième niveau de la classification Hema. Lors du changement de classification de premier niveau, il est nécessaire d'actualiser et de restituer toutes les classifications de deuxième niveau. Toutes sont considérées comme des observateurs polluants. En tant que même ViewType , seuls recyclerPool5 sont mis en cache par défaut, et c'est proche de 10 sur la figure. Pour un, ce n'est pas suffisant. Sur la base de données suffisantes, presque chaque changement nécessite createenviron 5 spectateurs. Développez ensuite le cache de ce type ( setMaxRecycledViews), alors seule une nouvelle liaison est requise

Ensuite, mCachedViewsquand est-il recommandé d'augmenter la capacité, je l'ai personnellement deviné, peut-être comme les cascades et les grilles, il peut y en avoir plusieurs qui sont supprimées en même temps lorsque vous glissez vers le haut d'une rangée, puis vous pouvez la restaurer rapidement en glissant vers le bas une ligne (comme les grilles, les grilles, etc.) Elle est définie sur 2+spanCount par défaut) pour éviter d'être partiellement recyclée dans le recyclerPool pour une nouvelle liaison en raison d'un débordement

2. Dans quelles circonstances le cache récupéré a-t-il besoin de onBind, et quand n'en a-t-il pas besoin ?

Nous avons mentionné plus tôt que ceux qui RecycledViewPoolen doivent être resetInternalréinitialisés et qu'une nouvelle liaison doit être effectuée. Et mChangedScrapconfirmez que la mise à jour doit être distinguée, donc une nouvelle liaison est également requise

Le reste mAttachedScrapet la somme mCachedViewssont jugés non inclus FLAG_UPDATE, ils ne sont donc naturellement pas nécessaires. Bien sûr, c'est aussi leur limite, car il ne peut être utilisé que pour le multiplexage à la même position.

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

3. Quelques autres suggestions d'optimisation pour RV

  1. Si la taille du VR est fixe, cela peut RecyclerView.setHasFixedSize(true)éviter requestLayoutune consommation inutile de performances en
// RecyclerViewDataObserver.java
void triggerUpdateProcessor() {
    if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
        RecyclerView.this.postOnAnimation(mUpdateChildViewsRunnable);
    } else {
        mAdapterUpdateDuringMeasure = true;
        requestLayout();
    }
}
  1. Supprimer l'animation par défaut (selon la situation)
  2. onBindViewHolder ne lie que autant que possible pour réduire le traitement des données
  3. Utiliser la charge utile pour actualiser partiellement, réduire les autres liaisons de données inutiles de onBindViewHolder, peut être combiné avec DiffUtil
  4. Surveillez le glissement et annulez les opérations telles que le chargement de l'image lors du glissement, telles queGlide.with(mContext).pauseRequests()

4. Non recommandé pour notifyDataSetChanged

Appeler markKnownViewsInvalidla méthode avec toutes ViewHolderles marques FLAG_INVALID, cela signifie que le lien est détaché et que tous les éléments seront recyclés dans le recyclerViewPool. Si le pool déborde, il doit être créé pour provoquer une consommation lors du remplissage.

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

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

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

Notes d'étude sur Android

Optimisation des performances Android : https://qr18.cn/FVlo89
principes sous-jacents du framework Android : https://qr18.cn/AQpN4J
bucket de la famille Jetpack (y compris Compose) : https://qr18.cn/A0gajp
voiture Android : https://qr18.cn/F05ZCM
notes d'étude de sécurité inversée Android : https://qr18.cn/CQ5TcL
audio et vidéo Android : https://qr18.cn/Ei3VPD
Kotlin : https://qr18.cn/CdjtAF
Gradle : https://qr18.cn/DzrmMB
notes d'analyse du code source OkHttp : https://qr18.cn/Cw0pBD
Flutter : https://qr18.cn/DIvKma
Android Eight Knowledge Body : https://qr18.cn/CyxarU
Android Core Notes : https://qr21.cn/CaZQLo
Questions d'entretien passées sur Android : https://qr18.cn/CKV8OZ
Dernière collection de questions d'entretien Android 2023 : https://qr18.cn/CgxrRy
Exercices d'entretien d'embauche pour le développement de véhicules Android : https://qr18.cn/FTlyCJ
Questions d'entretien audio et vidéo :https://qr18.cn/AcV6Ap

Je suppose que tu aimes

Origine blog.csdn.net/weixin_61845324/article/details/132602220
conseillé
Classement