Desempenho de rolagem do Android RecyclerView

86

Criei um exemplo de RecyclerView com base no guia Criação de listas e cartões . Meu adaptador tem uma implementação de padrão apenas para aumentar o layout.

O problema é o baixo desempenho de rolagem. Isso em um RecycleView com apenas 8 itens.

Em alguns testes verifiquei que no Android L esse problema não ocorre. Mas na versão KitKat a diminuição do desempenho é evidente.

falvojr
fonte
1
Tente usar o padrão de design ViewHolder para desempenho de rolagem: developer.android.com/training/improving-layouts/…
Haresh Chhelana
@HareshChhelana obrigado pela resposta! Mas já estou usando o padrão ViewHolder, de acordo com o link: developer.android.com/training/material/lists-cards.html
falvojr
2
Você pode compartilhar algum código sobre a configuração do adaptador e o arquivo XML para seus layouts. Isso não parece normal. Além disso, você fez o perfil e viu onde o tempo é gasto?
yigit
2
Estou enfrentando quase os mesmos problemas. Exceto que é rápido no pré Lollipop e incrível (realmente) lento no Android L.
Servus7
1
você também pode compartilhar a versão da biblioteca que está importando.
Droidekas

Respostas:

227

Recentemente enfrentei o mesmo problema, então isso é o que fiz com a biblioteca de suporte RecyclerView mais recente:

  1. Substitua um layout complexo (visualizações aninhadas, RelativeLayout) pelo novo ConstraintLayout otimizado. Ative-o no Android Studio: Vá para SDK Manager -> guia SDK Tools -> Support Repository -> marque ConstraintLayout para Android e Solver para ConstraintLayout. Adicione às dependências:

    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    
  2. Se possível, faça todos os elementos do RecyclerView com a mesma altura . E adicione:

    recyclerView.setHasFixedSize(true);
    
  3. Use os métodos de cache de desenho padrão do RecyclerView e ajuste-os de acordo com o seu caso. Você não precisa de uma biblioteca de terceiros para fazer isso:

    recyclerView.setItemViewCacheSize(20);
    recyclerView.setDrawingCacheEnabled(true);
    recyclerView.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);
    
  4. Se você usar muitas imagens , verifique se o tamanho e a compactação são ideais . O dimensionamento de imagens também pode afetar o desempenho. O problema tem dois lados - a imagem de origem usada e o bitmap decodificado. O exemplo a seguir dá uma dica de como decodificar uma imagem baixada da web:

    InputStream is = (InputStream) url.getContent();
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inPreferredConfig = Bitmap.Config.RGB_565;
    Bitmap image = BitmapFactory.decodeStream(is, null, options);
    

A parte mais importante é especificar inPreferredConfig- define quantos bytes serão usados ​​para cada pixel da imagem. Lembre-se de que essa é a opção preferencial . Se a imagem de origem tiver mais cores, ela ainda será decodificada com uma configuração diferente.

  1. Certifique-se de que onBindViewHolder () seja o mais barato possível. Você pode definir OnClickListener uma vez dentro onCreateViewHolder()e chamar por meio de uma interface um ouvinte fora do conector , passando o item clicado. Dessa forma, você não cria objetos extras o tempo todo. Verifique também sinalizadores e estados, antes de fazer qualquer alteração na visualização aqui.

    viewHolder.itemView.setOnClickListener(new View.OnClickListener() {
          @Override
          public void onClick(View view) {
              Item item = getItem(getAdapterPosition());
              outsideClickListener.onItemClicked(item);
          }
    });
    
  2. Quando os dados são alterados, tente atualizar apenas os itens afetados . Por exemplo, em vez de invalidar todo o conjunto de dados com notifyDataSetChanged(), ao adicionar / carregar mais itens, basta usar:

    adapter.notifyItemRangeInserted(rangeStart, rangeEnd);
    adapter.notifyItemRemoved(position);
    adapter.notifyItemChanged(position);
    adapter.notifyItemInserted(position);
    
  3. Do site do desenvolvedor Android :

Conte com notificarDataSetChanged () como último recurso.

Mas se você precisar usá-lo, mantenha seus itens com ids exclusivos :

    adapter.setHasStableIds(true);

O RecyclerView tentará sintetizar eventos de mudança estrutural visíveis para adaptadores que relatam que têm IDs estáveis ​​quando este método é usado. Isso pode ajudar para fins de animação e persistência de objetos visuais, mas as visualizações de itens individuais ainda precisarão ser reativadas e recarregadas.

Mesmo se você fizer tudo certo, é provável que o RecyclerView ainda não esteja funcionando tão bem quanto você gostaria.

Galya
fonte
18
um voto para adapter.setHasStableIds (true); método que realmente ajudou a tornar a visualização da reciclagem mais rápida.
Atula
1
A parte 7 está totalmente errada! setHasStableIds (true) não fará nada, exceto você usar adapter.notifyDataSetChanged (). Link: developer.android.com/reference/android/support/v7/widget/…
localhost
1
Eu posso ver porque o recyclerView.setItemViewCacheSize(20);pode melhorar o desempenho. Mas recyclerView.setDrawingCacheEnabled(true);e recyclerView.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);embora! Não tenho certeza se isso vai mudar alguma coisa. Essas são Viewchamadas específicas que permitem recuperar programaticamente o cache de desenho como um bitmap e usá-lo para sua vantagem posteriormente. RecyclerViewnão parece fazer nada a respeito.
Abdelhakim AKODADI
1
@AbdelhakimAkodadi, a rolagem fica mais suave com o cache. Eu testei. Como você poderia dizer o contrário, é óbvio. Claro, se alguém rolar como um louco, nada vai ajudar. Apenas mostro as outras opções, como setDrawingCacheQuality, que não uso, porque a qualidade da imagem é importante no meu caso. Eu não prego DRAWING_CACHE_QUALI‌ TY_HIGH, mas sugiro a quem estiver interessado que se aprofunde e ajuste a opção.
Galya
2
setDrawingCacheEnabled () e setDrawingCacheQuality () estão obsoletos. Em vez disso, use a aceleração de hardware. developer.android.com/reference/android/view/…
Shayan_Aryan
14

Resolvi esse problema adicionando o seguinte sinalizador:

https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html#setHasStableIds(boolean)

waclaw
fonte
2
Isso representa uma pequena melhora no desempenho para mim. Mas o RecyclerView ainda é lento demais - muito mais lento do que um ListView personalizado equivalente.
SMBiggs de
Ahh, mas descobri por que meu código é tão lento - não tem nada a ver com setHasStableIds (). Vou postar uma resposta com mais informações.
SMBiggs de
13

Descobri pelo menos um padrão que pode prejudicar seu desempenho. Lembre-se de que onBindViewHolder()é chamado com freqüência . Portanto, qualquer coisa que você fizer nesse código tem o potencial de interromper seu desempenho. Se o RecyclerView faz alguma personalização, é muito fácil colocar acidentalmente algum código lento neste método.

Eu estava mudando as imagens de fundo de cada RecyclerView dependendo da posição. Mas carregar imagens dá um pouco de trabalho, fazendo com que meu RecyclerView fique lento e irregular.

Criar um cache para as imagens funcionou maravilhosamente; onBindViewHolder()agora apenas modifica uma referência a uma imagem em cache em vez de carregá-la do zero. Agora o RecyclerView avança.

Eu sei que nem todo mundo terá esse problema exato, então não estou me preocupando em carregar o código. Mas, por favor, considere qualquer trabalho que seja feito em seu onBindViewHolder()como um gargalo potencial para o desempenho ruim do RecyclerView.

SMBiggs
fonte
Estou tendo o mesmo problema. No momento, estou usando o Fresco para carregar e armazenar imagens em cache. Você tem outra solução melhor para carregar e armazenar imagens em cache no RecyclerView. Obrigado.
Androidicus
Não estou familiarizado com Fresco (lendo ... suas promessas são boas). Talvez eles tenham alguns insights sobre como usar melhor seus caches com RecyclerViews. E vejo que você não é o único com esse problema: github.com/facebook/fresco/issues/414 .
SMBiggs
10

Além da resposta detalhada de @Galya, quero afirmar que, embora possa ser um problema de otimização, também é verdade que ter o depurador ativado pode tornar as coisas muito lentas.

Se você fizer tudo para otimizar seu RecyclerViewe ainda não funcionar sem problemas, tente alternar sua variante de compilação para releasee verifique como funciona em um ambiente de não desenvolvimento (com o depurador desativado)

Aconteceu comigo que meu aplicativo estava funcionando lentamente na debugvariante de compilação, mas assim que mudei para a releasevariante, ele funcionou bem. Isso não significa que você deve desenvolver com a releasevariante de compilação, mas é bom saber que, sempre que estiver pronto para enviar seu aplicativo, ele funcionará perfeitamente.

Blastervla
fonte
Este comentário foi muito útil para mim! Eu tentei de tudo para melhorar o desempenho do meu Recyclerview, mas nada realmente ajudou, mas assim que mudei para a versão de lançamento, percebi que está tudo bem.
Tal Barda
1
Eu enfrentei o mesmo problema, mesmo sem o depurador anexado, mas assim que movi para liberar a compilação, o problema não foi mais visto
Farmaan Elahi
8

Eu conversei sobre RecyclerViewo desempenho de. Aqui estão os slides em inglês e o vídeo gravado em russo .

Ele contém um conjunto de técnicas (algumas delas já foram abordadas pela resposta de @Darya ).

Aqui está um breve resumo:

  • Se os Adapteritens tiverem tamanho fixo, defina:
    recyclerView.setHasFixedSize(true);

  • Se as entidades de dados puderem ser representadas por long ( hashCode()por exemplo), defina:
    adapter.hasStableIds(true);
    e implemente:
    // YourAdapter.java
    @Override
    public long getItemId(int position) {
    return items.get(position).hashcode(); //id()
    }
    Nesse caso Item.id(), não funcionaria, porque permaneceria o mesmo mesmo se Itemo conteúdo de fosse alterado.
    PS Isso não é necessário se você estiver usando o DiffUtil!

  • Use bitmap dimensionado corretamente. Não reinvente a roda e use bibliotecas.
    Mais informações sobre como escolher aqui .

  • Sempre use a versão mais recente do RecyclerView. Por exemplo, houve grandes melhorias de desempenho em 25.1.0- prefetch.
    Mais informações aqui .

  • Use DiffUtill.
    O DiffUtil é obrigatório .
    Documentação oficial .

  • Simplifique o layout do seu item!
    Biblioteca minúscula para enriquecer TextViews - TextViewRichDrawable

Veja os slides para uma explicação mais detalhada.

Oleksandr
fonte
7

Não tenho certeza se o uso do setHasStableIdsinalizador resolverá seu problema. Com base nas informações fornecidas, seu problema de desempenho pode estar relacionado a um problema de memória. O desempenho do seu aplicativo em termos de interface de usuário e memória está bastante relacionado.

Na semana passada, descobri que meu aplicativo estava perdendo memória. Descobri isso porque, após 20 minutos de uso do meu aplicativo, percebi que o desempenho da IU estava muito lento. Fechar / abrir uma atividade ou rolar um RecyclerView com um monte de elementos era muito lento. Depois de monitorar alguns dos meus usuários em produção usando http://flowup.io/ , descobri o seguinte:

insira a descrição da imagem aqui

O tempo de fotogramas foi muito alto e os fotogramas por segundo muito baixos. Você pode ver que alguns quadros precisaram de cerca de 2 segundos para renderizar: S.

Tentando descobrir o que estava causando esse tempo / fps de quadro ruim, descobri que tinha um problema de memória, como você pode ver aqui:

insira a descrição da imagem aqui

Mesmo quando o consumo médio de memória estava próximo dos 15 MB, ao mesmo tempo, o aplicativo estava perdendo frames.

Foi assim que descobri o problema da IU. Eu tive um vazamento de memória em meu aplicativo causando muitos eventos de coletor de lixo e isso estava causando o mau desempenho da IU porque a VM Android teve que parar meu aplicativo para coletar memória a cada quadro.

Olhando para o código, tive um vazamento dentro de uma visualização personalizada porque não estava cancelando o registro de um ouvinte da instância do Android Choreographer. Depois de lançar a correção, tudo voltou ao normal :)

Se seu aplicativo está perdendo frames devido a um problema de memória, você deve revisar dois erros comuns:

Verifique se o seu aplicativo está alocando objetos dentro de um método invocado várias vezes por segundo. Mesmo que essa alocação possa ser realizada em um local diferente, onde seu aplicativo esteja se tornando lento. Um exemplo poderia ser a criação de novas instâncias de um objeto dentro de um método de visualização personalizada onDraw em onBindViewHolder em seu suporte de visualização do reciclador. Verifique se o seu aplicativo está registrando uma instância no Android SDK, mas não está liberando-a. Registrar um ouvinte em um evento de barramento também pode ser um vazamento possível.

Isenção de responsabilidade: a ferramenta que uso para monitorar meu aplicativo está em desenvolvimento. Eu tenho acesso a essa ferramenta porque sou um dos desenvolvedores :) Se você quiser ter acesso a essa ferramenta, lançaremos uma versão beta em breve! Você pode participar em nosso site: http://flowup.io/ .

Se você quiser usar ferramentas diferentes, pode usar: traveview, dmtracedump, systrace ou o monitor de desempenho Andorid integrado ao Android Studio. Mas lembre-se de que essas ferramentas irão monitorar seu dispositivo conectado e não o resto de seus dispositivos de usuário ou instalações do sistema operacional Android.

Pedro Vicente Gómez Sánchez
fonte
3

Também é importante verificar o layout pai no qual você colocou em sua Recyclerview. Tive problemas de rolagem semelhantes ao testar o recyclerView em um nestedscrollview. Uma visão que rola em outra visão que pode sofrer no desempenho durante a rolagem

Saintjab
fonte
Sim ponto válido. Eu também enfrento o mesmo problema!
Ranjith Kumar
2

No meu caso, descobri que a causa notável do atraso é o #onBindViewHolder()método de carregamento freqüente de drawable . Resolvi apenas carregando as imagens como Bitmap uma vez dentro do ViewHolder e acessando-as a partir do método mencionado. Isso é tudo que eu fiz.

Wei Chan
fonte
2

No meu RecyclerView, eu uso imagens de bitmap para o plano de fundo do meu item_layout.
Tudo o que @Galya disse é verdade (e agradeço a ele por sua ótima resposta). Mas eles não funcionaram para mim.

Isso é o que resolveu meu problema:

BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2;
Bitmap bitmap = BitmapFactory.decodeStream(stream, null, options);

Para obter mais informações, leia esta resposta .

mohandesR
fonte
2

No meu caso, tenho crianças complexas com visão de reciclagem. Portanto, isso afetou o tempo de carregamento da atividade (~ 5 segundos para a renderização da atividade)

Eu carrego o adaptador com postDelayed () -> isso dará um bom resultado para a renderização da atividade. após a atividade de renderização de meu carregamento de reciclagem com suavização

Tente esta resposta,

    recyclerView.postDelayed(new Runnable() {
        @Override
        public void run() {
            recyclerView.setAdapter(mAdapter);
        }
    },100); 
Ranjith Kumar
fonte
1

Vejo nos comentários que você já está implementando o ViewHolderpadrão, mas postarei um exemplo de adaptador aqui que usa o RecyclerView.ViewHolderpadrão para que você possa verificar se está integrando de forma semelhante, novamente seu construtor pode variar dependendo de suas necessidades, aqui é um exemplo:

public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerAdapter.ViewHolder> {

    Context mContext;
    List<String> mNames;

    public RecyclerAdapter(Context context, List<String> names) {
        mContext = context;
        mNames = names;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
        View view = LayoutInflater.from(viewGroup.getContext())
                .inflate(android.R.layout.simple_list_item_1, viewGroup, false);

        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(ViewHolder viewHolder, int position) {
        //Populate.
        if (mNames != null) {
            String name = mNames.get(position);

            viewHolder.name.setText(name);
        }
    }

    @Override
    public int getItemCount() {

        if (mNames != null)
            return mNames.size();
        else
            return 0;
    }

    /**
     * Static Class that holds the RecyclerView views. 
     */
    static class ViewHolder extends RecyclerView.ViewHolder {
        TextView name;

        public ViewHolder(View itemView) {
            super(itemView);
            name = (TextView) itemView.findViewById(android.R.id.text1);
        }
    }
}

Se você tiver qualquer problema de trabalho, RecyclerView.ViewHoldercertifique-se de ter as dependências apropriadas, que você pode verificar sempre no Gradle.

Espero que isso resolva seu problema.

Joel
fonte
1

Isso me ajudou a obter uma rolagem mais suave:

substituir o onFailedToRecycleView (suporte do ViewHolder) no adaptador

e interromper todas as animações em andamento (se houver). "animateview" .clearAnimation ();

lembre-se de retornar verdadeiro;

Roar Grønmo
fonte
1

Eu resolvi com esta linha de código

recyclerView.setNestedScrollingEnabled(false);
eli
fonte
1

Adicionando à resposta de @Galya, no bind viewHolder, eu estava usando o método Html.fromHtml (). aparentemente, isso tem impacto no desempenho.

Sami Adam
fonte
0

i Resolva esse problema usando a única linha da biblioteca de Picasso

.em forma()

Picasso.get().load(currentItem.getArtist_image())

                    .fit()//this wil auto get the size of image and reduce it 

                    .placeholder(R.drawable.ic_doctor)
                    .into(holder.img_uploaderProfile, new Callback() {
                        @Override
                        public void onSuccess() {


                        }

                        @Override
                        public void onError(Exception e) {
                            Toast.makeText(context, "Something Happend Wrong Uploader Image", Toast.LENGTH_LONG).show();
                        }
                    });
Hamza independentemente
fonte