Um artigo para obter "Mecanismo de reutilização de cache do RecyclerView"

prefácio

Neste artigo, não adicionaremos o pré-carregamento por enquanto. Entenda o mecanismo de cache primeiro e depois adicione o pré-carregamento para entendê-lo rapidamente. Pode ser confuso se você misturá-los.

Zero, por que cache

RecyclerView é um novo controle de interface do usuário proposto pelo Android 5.0. Como o nome sugere, ele reciclará sua visualização de itens de lista para reutilização.
Especificamente, quando um item de lista é removido da tela, o RecyclerView não destrói sua exibição, mas o armazena em cache para reutilização por novos itens de lista que entram na tela. Essa reutilização pode:

  • Evite a criação duplicada de exibições desnecessárias
  • Evite a execução repetida do caro findViewById
    para melhorar o desempenho, melhorar a capacidade de resposta do aplicativo e reduzir o consumo de energia. Para entender como funciona, temos que voltar a como o RecyclerView constrói uma lista dinâmica.

1. Como o RecyclerView cria nossa exibição de lista

Entre várias classes importantes associadas ao RecyclerView para construir listas dinâmicas, Adapter e ViewHolder são responsáveis ​​por cooperar para definir o modo de exibição dos dados dos itens da lista RecyclerView, entre os quais:

  • ViewHolder é um "contêiner wrapper contendo exibição de item de lista (itemView)" e "o objeto principal da reutilização do cache RecyclerView".
  • Adapter fornece o relacionamento "binding" de "data<->view", que inclui os seguintes métodos principais:
    • onCreateViewHolder: Responsável por criar e inicializar o ViewHolder e suas views associadas, mas não preenche o conteúdo da view.
    • onBindViewHolder: Responsável por extrair os dados apropriados e preencher o conteúdo da view do ViewHolder.
      No entanto, esses dois métodos não chamam de volta todos os itens da lista que entram na tela, pelo contrário, como ações como criação de view e execução de findViewById estão concentradas principalmente nesses dois métodos, não é eficiente chamar de volta todas as vezes. Portanto, devemos minimizar a frequência de retornos de chamada para esses dois métodos, armazenando ativamente em cache e reutilizando o objeto ViewHolder.
  1. A situação ideal é - o objeto de cache obtido passa a ser o objeto ViewHolder original. Nesse caso, não há necessidade de recriar o objeto, nem de religar os dados, e ele pode ser usado imediatamente.
  2. A situação abaixo do ideal é - embora o objeto de cache obtido não seja o objeto ViewHolder original, mas porque os dois tipos de item de lista (itemType) são os mesmos, as exibições associadas podem ser reutilizadas, portanto, apenas os dados precisam ser vinculados novamente .
  3. No final, realmente não há outro jeito, então precisamos executar o callback desses dois métodos, ou seja, criar um novo objeto ViewHolder e vincular os dados. Na verdade, essa também é a ordem de prioridade que RecyclerView segue ao procurar o melhor objeto ViewHolder correspondente no cache. E o que realmente é responsável por realizar essa busca é uma classe interna chamada "Recycler" na classe RecyclerView-Recycler.

2. Processo de cache

Então vamos ver desde o início como encontrar nossa view (porque é mais intuitivo de ver, então não adicionamos a lógica de pré-carregamento primeiro) Lembre-se que configuramos
nosso layoutManager quando criamos nosso RecyclerView. Na verdade, a adição e construção de todas as Views em nossa lista são iniciadas pelo LayoutManager que definimos. Aqui está apenas uma explicação para o gerenciador de layout do LinearLayoutManager.
insira a descrição da imagem aqui
Obviamente, nosso RecyclerView suporta LinearLayoutManager comum, grade GridLayoutManager, fluxo em cascata StaggeredGridLayoutManager, streaming FlexboxLayoutManager, LayoutManager personalizado.
Mas, no final, usaremos o mecanismo de retorno em nosso Recycle, que é o método importante: tryGetViewHolderForPositionByDeadline
insira a descrição da imagem aqui
Este método tentará obter o ViewHolder na posição especificada pesquisando ou criando-o diretamente a partir do sucata do Recycler, cache, RecycledViewPool.

public final class Recycler {
    
    
        ...
        /**
         * Attempts to get the ViewHolder for the given position, either from the Recycler scrap,
         * cache, the RecycledViewPool, or creating it directly.
         * 
         * 尝试通过从Recycler scrap缓存、RecycledViewPool查找或直接创建的形式来获取指定位置的ViewHolder。
         * ...
         */
        @Nullable
        ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
    
    
            if (mState.isPreLayout()) {
    
    
                // 0 尝试从mChangedScrap中获取ViewHolder对象
                holder = getChangedScrapViewForPosition(position);
                ...
            }
            if (holder == null) {
    
    
                // 1.1 尝试根据position从mAttachedScrap或mCachedViews中获取ViewHolder对象
                holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
                ...
            }
            if (holder == null) {
    
    
                ...
                final int type = mAdapter.getItemViewType(offsetPosition);
                if (mAdapter.hasStableIds()) {
    
    
                    // 1.2 尝试根据id从mAttachedScrap或mCachedViews中获取ViewHolder对象
                    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                            type, dryRun);
                    ...
                }
                if (holder == null && mViewCacheExtension != null) {
    
    
                    // 2 尝试从mViewCacheExtension中获取ViewHolder对象
                    final View view = mViewCacheExtension
                            .getViewForPositionAndType(this, position, type);
                    if (view != null) {
    
    
                        holder = getChildViewHolder(view);
                        ...
                    }
                }
                if (holder == null) {
    
     // fallback to pool
                    // 3 尝试从mRecycledViewPool中获取ViewHolder对象
                    holder = getRecycledViewPool().getRecycledView(type);
                    ...
                }
                if (holder == null) {
    
    
                    // 4.1 回调createViewHolder方法创建ViewHolder对象及其关联的视图
                    holder = mAdapter.createViewHolder(RecyclerView.this, type);
                    ...
                }
            }
    
            if (mState.isPreLayout() && holder.isBound()) {
    
    
                ...
            } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
    
    
                ...
                // 4.1 回调bindViewHolder方法提取数据填充ViewHolder的视图内容
                bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
            }
    
            ...
    
            return holder;
        }
        ...
    }    

Combinado com o código-fonte e comentários na classe RecyclerView, pode-se saber que Recycler tentará obter objetos ViewHolder em locais ou IDs especificados de mChangedScrap/mAttachedScrap, mCachedViews, mViewCacheExtension e mRecyclerPool por sua vez para reutilização. Se nenhum deles puder obtidos, eles serão recriados diretamente. As várias camadas de estruturas de cache envolvidas são:

3. Estrutura de cache

1, mChangedScrap/mAttachedScrap

mChangedScrap/mAttachedScrap é usado principalmente para "armazenamento temporário de itens de lista que ainda estão visíveis na tela atual, mas marcados como "removidos" ou "reutilizados". Todos eles contêm o objeto ViewHolder de cada item de lista na forma de ArrayList, o tamanho Não há limite claro, mas, em geral, o número máximo é o número total de itens de lista visíveis na tela.

final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
ArrayList<ViewHolder> mChangedScrap = null;

Mas aí vem a pergunta, já que é um item de lista que está visível na tela no momento, por que ele precisa ser armazenado em cache? E quando um item da lista será marcado como "removido" ou "reutilizado"?
Essas duas estruturas de cache são, na verdade, mais para evitar operações como "atualização parcial" que fazem com que todos os itens da lista sejam redesenhados.
A diferença é que os principais cenários de uso do mChangedScrap são:

  1. A animação do item da lista (itemAnimator) está habilitada e o método canReuseUpdatedViewHolder(ViewHolder viewHolder) da animação do item da lista retorna false;
  2. Chame notifyItemChanged, notifyItemRangeChanged e outros métodos para notificar alterações de dados de itens de lista;
boolean canReuseUpdatedViewHolder(ViewHolder viewHolder) {
    
    
        return mItemAnimator == null || mItemAnimator.canReuseUpdatedViewHolder(viewHolder,
                viewHolder.getUnmodifiedPayloads());
    }
    
    public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder,
            @NonNull List<Object> payloads) {
    
    
        return canReuseUpdatedViewHolder(viewHolder);
    }
        
    public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder) {
    
    
        return true;
    }

Os diferentes significados do valor de retorno do método canReuseUpdatedViewHolder são os seguintes:

  • true, indicando que o objeto ViewHolder original pode ser reutilizado
  • false, indicando que uma cópia deste ViewHolder deve ser criada para que itemAnimator utilize ambos para obter efeitos de animação (como efeitos de crossfade).

Simplificando, mChangedScrap é principalmente para o efeito de animação quando os dados do item da lista são alterados.
E o mAttachedScrap lida com a maioria dos cenários restantes, como:

  • Itens de lista como notifyItemMoved e notifyItemRemoved são movidos, mas os próprios dados do item de lista não são alterados.
  • A animação do item da lista é desativada ou o método canReuseUpdatedViewHolder da animação do item da lista retorna true, ou seja, a cena em que o objeto ViewHolder original pode ser reutilizado.

Vamos usar uma operação simples notifyItemRemoved(int position) como exemplo para demonstrar:
O método notifyItemRemoved(int position) é usado para notificar o observador que o item da lista na posição anterior foi removido e as posições subsequentes do item da lista serão todas movidas avançar Mover 1 bit.
Para simplificar o problema e facilitar a demonstração, nossos exemplos estarão sujeitos às seguintes restrições:

  • O número total de itens da lista não cobre a tela inteira, o que significa que as operações de cache de estruturas como mCachedViews e mRecyclerPool não serão acionadas
  • Remover a animação do item da lista - significa que o RecyclerView só redefinirá o layout das subvisualizações uma vez depois de chamar o notifyItemRemoved

recyclerView.itemAnimator = null

Idealmente, depois de chamar o método notifyItemRemoved(int position), apenas o item da lista na posição deve ser removido, e os outros itens da lista, estejam eles localizados antes ou depois da posição, ajustarão apenas o valor da posição no máximo e não devem acontecer. a recriação da exibição ou a religação dos dados, ou seja, os dois métodos de onCreateViewHolder e onBindViewHolder não devem ser chamados de volta.
Para esse fim, precisamos retirar temporariamente os itens da lista visível na tela atual da tela atual e armazená-los temporariamente em cache na estrutura mAttachedScrap.
insira a descrição da imagem aqui
Aguarde até que RecyclerView reinicie o layout para exibir suas subvisualizações e, em seguida, atravesse mAttachedScrap para localizar o objeto ViewHolder correspondente à posição para reutilização.
insira a descrição da imagem aqui

2、mCachedViews

mCachedViews é usado principalmente para "armazenar itens de lista que foram removidos da tela, mas podem entrar novamente na tela em breve". Ele também contém o objeto ViewHolder de cada item da lista na forma de ArrayList e o limite de tamanho padrão é 2.

final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
int mViewCacheMax = DEFAULT_CACHE_SIZE;
static final int DEFAULT_CACHE_SIZE = 2;

Por exemplo, o fluxo de feed exibido em ordem de tempo de atualização, como o círculo de amigos, geralmente verificamos se há conteúdo em que estamos interessados ​​durante o deslizar rápido. Deslize para trás para visualizar o conteúdo anterior.
Neste cenário, o que se busca naturalmente é o tempo real e a completude da exibição do conteúdo anterior, e o usuário não deve reclamar de "deslizar um pouco e depois recarregar", ou seja, a exibição não deve ser recriada ou recarregado Religação de dados.
Utilizamos diversos fluxogramas para demonstrar esta situação:
Também para simplificar o problema e facilitar a descrição, nossos exemplos estarão sujeitos às seguintes restrições:

  • Desativar a pré-busca - significa que, quando você deslizar para cima mais tarde, não irá mais pré-buscar um item da lista "para ser inserido na área da tela" e colocá-lo no mCachedView

recyclerView.layoutManager?.isItemPrefetchEnabled = false

  • Existe apenas um tipo de item de lista, ou seja, todos os itens de lista possuem o mesmo itemType, que é 0 por padrão.
    Dividimos os itens da lista na figura em três áreas, que são a área que desliza para fora da tela, a área visível na tela e a área que está prestes a entrar na tela com o gesto de deslizar.
    insira a descrição da imagem aqui

1. Quando o item da lista com posição = 0 é movido para fora da tela com o gesto deslizante para cima, ele pode ser colocado diretamente porque a capacidade inicial de mCachedViews é 0; 2. Quando o item da lista com posição = 1 também é movido para
insira a descrição da imagem aqui
fora da tela, porque lá atinge o limite de capacidade padrão do mCachedViews, então você pode continuar a colocá-lo;
insira a descrição da imagem aqui
3. Neste momento, mude para deslizar para baixo e o item da lista com posição = 1 entrará novamente na tela e Recycler irá procurar por itens que podem ser reutilizados nesta posição de mAttachedScrap e mCachedViews por sua vez objeto ViewHolder,
4. mAttachedScrap não lida com esta situação, por isso não pode ser encontrado naturalmente. E o mCachedViews irá percorrer os objetos ViewHolder mantidos por si só, e comparar se o valor da posição do objeto ViewHolder é consistente com o valor da posição da posição a ser reutilizada. Se sim, o objeto ViewHolder será removido do mCachedViews e devolvido; 5. O obtido
aqui O objeto ViewHolder pode ser reutilizado diretamente, ou seja, atende a "melhor situação" mencionada acima.
insira a descrição da imagem aqui
6. Além disso, conforme o item da lista na posição=1 entra novamente na tela, o item da lista na posição=7 também será movido para fora da tela, e o item da lista nesta posição também entrará em mCachedViews, ou seja, RecyclerView é armazenado em cache bidirecionalmente.
insira a descrição da imagem aqui

3、mViewCacheExtension

mViewCacheExtension é usado principalmente para fornecer um nível de cache adicional que pode ser controlado livremente pelos desenvolvedores. É um uso não convencional, portanto não será discutido aqui.

4、mRecyclerPool

mRecyclerPool é usado principalmente para "armazenar itens de lista que excedem o limite de mCachedViews e são removidos da tela de acordo com diferentes itemTypes". . O objeto ViewHolder dos itens da lista, o limite de tamanho padrão do ArrayList para cada itemType é 5.

  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;
            long mCreateRunningAverageNs = 0;
            long mBindRunningAverageNs = 0;
        }
        SparseArray<ScrapData> mScrap = new SparseArray<>();
        ...
    }

Como o limite de tamanho padrão de mCachedViews é apenas 2, quando houver mais de 2 itens de lista que deslizam para fora da tela, os objetos ViewHolder serão removidos de mCachedViews na ordem de primeiro a entrar, primeiro a sair e colocados em RecycledViewPool de acordo com itemType. ArrayList.
A principal consideração dessa estrutura de cache é que, à medida que o número de itens da lista que são deslizados para fora da tela aumenta e a distância de deslizamento se torna cada vez mais distante, a possibilidade de entrar novamente na tela também é reduzida. Assim, o Recycler fez um trade-off entre tempo e espaço, permitindo que ViewHolders do mesmo itemType fossem extraídos e reutilizados, precisando apenas religar os dados.
Dessa forma, ele pode não apenas impedir que o cache de objeto ViewHolder em crescimento infinito ocupe o espaço de memória já apertado, mas também reduzir o número de retornos de chamada em comparação com o método onCreateViewHolder, mais caro.
Da mesma forma, usamos vários diagramas de fluxo para demonstrar essa situação, e esses diagramas continuarão a operar com base no diagrama mCachedViews anterior:
1. Assuma que os dois itens da lista que existem atualmente em mCachedViews ainda são position=0 e position=1 .
2. Quando continuamos a deslizar para cima, o item da lista com posição = 2 tentará entrar em mCachedViews. Como o limite de capacidade de mCachedViews foi excedido, o item da lista com posição = 0 será removido de mCachedViews e colocado em RecycledViewPool com itemType 0 ArrayList, que é a situação da figura ①;
insira a descrição da imagem aqui
3. Ao mesmo tempo, um novo item de lista na parte inferior também entrará na tela com o gesto deslizante, mas, neste momento, mAttachedScrap, mCachedViews e mRecyclerPool não possuem um objeto ViewHolder adequado para fornecer multiplexação, para que o item da lista possa executar apenas callbacks dos dois métodos onCreateViewHolder e onBindViewHolder, ou seja, a situação na figura ②;
4. Após o item da lista na posição=2 ser completamente removido da tela, ele entra no mCachedViews sem problemas.
insira a descrição da imagem aqui
5. Continuamos a deslizar para cima. Neste momento, como o próximo item da lista a entrar na tela tem o mesmo itemType do item da lista com posição = 0, podemos encontrar um objeto ViewHolder adequado de mRecyclerPool. Encontre o ArrayList correspondente de acordo com o itemType e, em seguida, retire um dos objetos ViewHolder para reutilização, ou seja, a situação ① na figura.
6. Como o itemType é do mesmo tipo, suas exibições associadas podem ser reutilizadas, portanto, você só precisa religar os dados, o que está de acordo com a "situação abaixo do ideal" mencionada acima.
insira a descrição da imagem aqui
7. ②③ A situação é consistente com as anteriores, e não se repetirá aqui.

4. Pré-carregamento

Por padrão, o RecyclerView implementa a função de pré-carregamento por meio do GapWorker para melhorar a fluência e a experiência do usuário ao deslizar.

O GapWorker fará uma pré-busca assíncrona de itens fora da tela quando o RecyclerView estiver rolando, preenchendo a área em branco da tela e garantindo que esses itens tenham sido pré-carregados quando precisarem ser exibidos. Essa implementação foi otimizada dentro do RecyclerView para garantir o preenchimento eficiente e preciso da parte em branco da área visível. Ao mesmo tempo, você pode controlar o número e a prioridade do pré-carregamento definindo alguns parâmetros de RecyclerView, como setInitialPrefetchItemCount() e setItemPrefetchEnabled().

Pré-carregamento O número de ViewHolders carregados por padrão é o número de telas na direção deslizante, ou seja, o número de ViewHolders na área visível do RecyclerView.

Por exemplo, se a direção do RecyclerView for deslizar verticalmente, cada pré-carregamento carregará o ViewHolder cuja área visível atual é uma tela acima e uma tela abaixo, ou seja, pré-carrega duas telas.

Se a direção do RecyclerView estiver deslizando horizontalmente, cada pré-carregamento carregará o ViewHolder cuja área visível atual é uma tela à esquerda e uma tela à direita, ou seja, pré-carrega duas telas.

Isso pode garantir que, quando o usuário deslizar para uma nova área, o ViewHolder pré-carregado esteja pronto e não haverá atraso, o que melhora a experiência do usuário. Obviamente, esse número pode ser modificado ajustando a propriedade RecyclerView.PREFETCHING_COUNT_DEFAULT.

Observe a imagem para entender:
1. Use a oportunidade quando o thread da interface do usuário estiver em um estado ocioso
insira a descrição da imagem aqui
2. Faça uma pré-busca e armazene em cache uma parte das exibições de itens da lista a serem inseridas na área da tela
insira a descrição da imagem aqui
3. Reduza operações demoradas, como como a criação de visualização ou vinculação de dados causada por Caton.

insira a descrição da imagem aqui
Então : nossa segunda camada de mCachedViews em cache não tem apenas nosso ViewHolder em cache fora da tela. E o ViewHolder que vamos puxar.
Objetos multiplexados em cache e objetos pré-buscados compartilham a mesma estrutura mCachedViews, mas os dois são armazenados em grupos, e objetos multiplexados em cache são classificados na frente dos objetos pré-buscados
insira a descrição da imagem aqui

mecanismo de pré-carga
conceito Quando o thread da interface do usuário está apenas no estado ocioso, uma parte da exibição do item da lista é puxada antecipadamente e armazenada em cache, reduzindo assim o atraso causado por operações demoradas, como criação de exibição ou vinculação de dados.
classe importante GapWorker: Constrói e agenda uma lista de tarefas de pré-busca com base em fatores como direção de deslizamento, velocidade de deslizamento e distância da área visível.
Reciclador: obtenha o objeto ViewHolder, se não for encontrado no cache, recrie e vincule
estrutura mCachedViews: obteve com sucesso o objeto ViewHolder e o colocou quando a vinculação de dados foi concluída
mRecyclerPool: obteve com sucesso o objeto ViewHolder, mas o colocou quando a vinculação de dados não foi concluída
tempo de iniciação Ao ser arrastado (Drag), deslizamento inercial (Fling) ou rolagem aninhada
Prazos Antes que o próximo sinal de sincronização vertical seja emitido

V. Resumo

Mecanismo de reutilização de cache RecyclerView
objeto ViewHolder (o wrapper que contém a exibição do item de lista (itemView))
Propósito Reduzir callbacks para os dois métodos onCreateViewHolder e onBindViewHolder
beneficiar 1. Evite a criação repetida de exibições desnecessárias 2. Evite a execução repetida de findViewById caro
Efeito Melhore o desempenho, aumente a capacidade de resposta do aplicativo e reduza o consumo de energia
classe principal Recycler, RecyclerViewPool
estrutura de cache mChangedScrap/mAttachedScrap, mCachedViews, mViewCacheExtension, mRecyclerPool
estrutura de cache Tipo de recipiente limite de capacidade finalidade do cache Se deve chamar de volta createView Se deve chamar de volta o bindView
mChangedScrap/mAttachedScrap ArrayList Nenhum, geralmente o número total de itens de lista visíveis na tela Mantém os itens da lista que foram movidos para fora da tela, mas podem entrar novamente na tela em breve não não
mCachedViews ArrayList O padrão é 2 Armazene temporariamente os itens da lista que ainda estão visíveis na tela atual, mas marcados para "remoção" ou "reutilização" não não
mViewCacheExtension os próprios desenvolvedores definem nenhum Forneça níveis de cache adicionais que podem ser controlados livremente pelos desenvolvedores não não
mRecyclerPool SparseArray Cada itemType tem como padrão 5 Itens da lista de armazenamento que excedem o limite de mCachedViews e são removidos da tela de acordo com diferentes tipos de item não sim

Acho que você gosta

Origin blog.csdn.net/weixin_45112340/article/details/130555601
Recomendado
Clasificación