Prestazioni di scorrimento di Android RecyclerView


88

Ho creato un esempio RecyclerView basandomi sulla guida alla creazione di elenchi e schede . Il mio adattatore ha un'implementazione del modello solo per gonfiare il layout.

Il problema sono le scarse prestazioni di scorrimento. Questo in un RecycleView con solo 8 elementi.

In alcuni test ho verificato che in Android L questo problema non si verifica. Ma nella versione KitKat il calo delle prestazioni è evidente.


1
Prova a utilizzare il modello di progettazione ViewHolder per le prestazioni di scorrimento: developer.android.com/training/improving-layouts/…
Haresh Chhelana

@HareshChhelana grazie per la tua risposta! Ma sto già usando il pattern ViewHolder, secondo il link: developer.android.com/training/material/lists-cards.html
falvojr

2
Puoi condividere del codice sulla configurazione dell'adattatore e il file XML per i tuoi layout. Questo non sembra normale. Inoltre, hai profilato e visto dove viene trascorso il tempo?
yigit

2
Sto affrontando quasi gli stessi problemi. Tranne che è veloce in pre Lollipop e incredibile (davvero) lento in Android L.
Servus7

1
puoi anche condividere la versione della libreria che stai importando.
Droidekas

Risposte:


231

Di recente ho riscontrato lo stesso problema, quindi questo è quello che ho fatto con l'ultima libreria di supporto RecyclerView:

  1. Sostituisci un layout complesso (viste nidificate, RelativeLayout) con il nuovo ConstraintLayout ottimizzato. Attivalo in Android Studio: vai su SDK Manager -> scheda SDK Tools -> Support Repository -> seleziona ConstraintLayout per Android e Solver per ConstraintLayout. Aggiungi alle dipendenze:

    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    
  2. Se possibile, rendere tutti gli elementi di RecyclerView con la stessa altezza . E aggiungi:

    recyclerView.setHasFixedSize(true);
    
  3. Usa i metodi di cache di disegno RecyclerView predefiniti e modificali in base al tuo caso. Non hai bisogno di una libreria di terze parti per farlo:

    recyclerView.setItemViewCacheSize(20);
    recyclerView.setDrawingCacheEnabled(true);
    recyclerView.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);
    
  4. Se utilizzi molte immagini , assicurati che le dimensioni e la compressione siano ottimali . Anche il ridimensionamento delle immagini può influire sulle prestazioni. Ci sono due lati del problema: l'immagine sorgente utilizzata e la bitmap decodificata. Il seguente esempio fornisce un suggerimento su come decodificare un'immagine, scaricata dal 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);
    

La parte più importante è specificare inPreferredConfig: definisce quanti byte verranno utilizzati per ogni pixel dell'immagine. Tieni presente che questa è un'opzione preferita . Se l'immagine sorgente ha più colori, verrà comunque decodificata con una configurazione diversa.

  1. Assicurati che onBindViewHolder () sia il più economico possibile. È possibile impostare OnClickListener una volta dentro onCreateViewHolder()e chiamare tramite un'interfaccia un listener esterno all'adattatore , passando l'elemento su cui si è fatto clic. In questo modo non crei sempre oggetti extra. Controlla anche flag e stati, prima di apportare modifiche alla visualizzazione qui.

    viewHolder.itemView.setOnClickListener(new View.OnClickListener() {
          @Override
          public void onClick(View view) {
              Item item = getItem(getAdapterPosition());
              outsideClickListener.onItemClicked(item);
          }
    });
    
  2. Quando i dati vengono modificati, prova ad aggiornare solo gli elementi interessati . Ad esempio, invece di invalidare l'intero set di dati con notifyDataSetChanged(), quando si aggiungono / caricano più elementi, utilizzare semplicemente:

    adapter.notifyItemRangeInserted(rangeStart, rangeEnd);
    adapter.notifyItemRemoved(position);
    adapter.notifyItemChanged(position);
    adapter.notifyItemInserted(position);
    
  3. Dal sito Web per sviluppatori Android :

Affidati a notifyDataSetChanged () come ultima risorsa.

Ma se hai bisogno di usarlo, mantieni i tuoi articoli con ID univoci :

    adapter.setHasStableIds(true);

RecyclerView tenterà di sintetizzare eventi di modifica strutturale visibili per gli adattatori che segnalano di avere ID stabili quando viene utilizzato questo metodo. Questo può aiutare ai fini dell'animazione e della persistenza degli oggetti visivi, ma le viste dei singoli oggetti dovranno comunque essere rimbalzate e rilanciate.

Anche se fai tutto bene, è probabile che RecyclerView non funzioni ancora nel modo corretto come vorresti.


20
un voto per adapter.setHasStableIds (true); metodo che ha davvero aiutato a rendere veloce la revisione del riciclaggio.
Atula

1
La parte 7 è totalmente sbagliata! setHasStableIds (true) non farà nulla a meno che non utilizzi adapter.notifyDataSetChanged (). Link: developer.android.com/reference/android/support/v7/widget/…
localhost

1
Capisco perché recyclerView.setItemViewCacheSize(20);riescono a migliorare le prestazioni. Ma recyclerView.setDrawingCacheEnabled(true);e recyclerView.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);comunque! Non sono sicuro che questi cambieranno qualcosa. Si tratta di Viewchiamate specifiche che consentono di recuperare a livello di codice la cache di disegno come bitmap e utilizzarla in seguito a proprio vantaggio. RecyclerViewnon sembra fare nulla al riguardo.
Abdelhakim AKODADI

2
@AbdelhakimAkodadi, lo scorrimento diventa fluido con la cache. L'ho provato. Come potresti dire altrimenti, è ovvio. Ovviamente, se qualcuno scorre come un matto, niente aiuterà. Mostro solo le altre opzioni come setDrawingCacheQuality, che non uso, perché la qualità dell'immagine è importante nel mio caso. Non predico DRAWING_CACHE_QUALI‌ TY_HIGH, ma suggerisco a chiunque sia interessato di approfondire e modificare l'opzione.
Galya

2
setDrawingCacheEnabled () e setDrawingCacheQuality () sono deprecati. Usa invece l'accelerazione hardware. developer.android.com/reference/android/view/…
Shayan_Aryan

14

2
Questo migliora leggermente le prestazioni per me. Ma RecyclerView è ancora lento, molto più lento di un ListView personalizzato equivalente.
SMBiggs

Ahh, ma ho scoperto perché il mio codice è così lento: non ha nulla a che fare con setHasStableIds (). Pubblicherò una risposta con maggiori informazioni.
SMBiggs

13

Ho scoperto almeno uno schema che può uccidere la tua performance. Ricorda che onBindViewHolder()viene chiamato frequentemente . Quindi qualsiasi cosa tu faccia in quel codice ha il potenziale per bloccare le tue prestazioni. Se il tuo RecyclerView esegue qualsiasi personalizzazione, è molto facile inserire accidentalmente del codice lento in questo metodo.

Stavo cambiando le immagini di sfondo di ogni RecyclerView a seconda della posizione. Ma il caricamento delle immagini richiede un po 'di lavoro, rendendo il mio RecyclerView lento e a scatti.

La creazione di una cache per le immagini ha funzionato a meraviglia; onBindViewHolder()ora modifica solo un riferimento a un'immagine memorizzata nella cache invece di caricarla da zero. Ora il RecyclerView si avvia.

So che non tutti avranno questo problema esatto, quindi non mi preoccupo di caricare il codice. Tuttavia, considera qualsiasi lavoro svolto nel tuo onBindViewHolder()come potenziale collo di bottiglia per prestazioni scadenti di RecyclerView.


Ho lo stesso problema. In questo momento sto usando Fresco per il caricamento delle immagini e la memorizzazione nella cache. Hai un'altra soluzione migliore per caricare e memorizzare nella cache le immagini in RecyclerView. Grazie.
Androidicus

Non ho familiarità con Fresco (leggendo ... le loro promesse sono belle). Forse hanno alcune informazioni su come utilizzare al meglio le loro cache con RecyclerViews. E vedo che non sei l'unico con questo problema: github.com/facebook/fresco/issues/414 .
SMBiggs

10

Oltre alla risposta dettagliata di @ Galya, voglio affermare che anche se potrebbe essere un problema di ottimizzazione, è anche vero che avere il debugger abilitato può rallentare molto le cose.

Se fai di tutto per ottimizzare il tuo RecyclerViewe ancora non funziona correttamente, prova a cambiare la tua variante di build releasee controlla come funziona in un ambiente non di sviluppo (con il debugger disabilitato).

Mi è capitato che la mia app funzionasse lentamente nella debugvariante build, ma non appena sono passato alla releasevariante ha funzionato senza problemi. Ciò non significa che dovresti sviluppare con la releasevariante build, ma è bene sapere che ogni volta che sei pronto per spedire la tua app, funzionerà perfettamente.


Questo commento è stato davvero utile per me! Ho provato di tutto per migliorare le prestazioni del mio recyclerview ma niente mi ha aiutato davvero, ma una volta passato alla build di rilascio ho capito che tutto andava bene.
Tal Barda

1
Ho affrontato lo stesso problema anche senza il debugger collegato, ma non appena sono passato alla build di rilascio, il problema non si è più visto
Farmaan Elahi

8

Ho parlato della RecyclerViewperformance di. Qui ci sono diapositive in inglese e video registrati in russo .

Contiene una serie di tecniche (alcune di esse sono già coperte dalla risposta di @ Darya ).

Ecco un breve riassunto:

  • Se gli Adapterelementi hanno dimensioni fisse, impostare:
    recyclerView.setHasFixedSize(true);

  • Se le entità di dati possono essere rappresentate da long ( hashCode()per esempio) allora imposta:
    adapter.hasStableIds(true);
    e implementa:
    // YourAdapter.java
    @Override
    public long getItemId(int position) {
    return items.get(position).hashcode(); //id()
    }
    In questo caso Item.id()non funzionerebbe, perché rimarrebbe lo stesso anche se Itemil contenuto di è cambiato.
    PS Questo non è necessario se stai usando DiffUtil!

  • Usa bitmap ridimensionato correttamente. Non reinventare la ruota e utilizzare le librerie.
    Maggiori informazioni su come scegliere qui .

  • Usa sempre l'ultima versione di RecyclerView. Ad esempio, ci sono stati enormi miglioramenti delle prestazioni in 25.1.0- prefetch.
    Maggiori info qui .

  • Usa DiffUtill.
    DiffUtil è un must .
    Documentazione ufficiale .

  • Semplifica il layout del tuo articolo!
    Piccola libreria per arricchire TextViews - TextViewRichDrawable

Vedere le diapositive per una spiegazione più dettagliata.


7

Non sono davvero sicuro che l'uso di setHasStableIdflag risolverà il tuo problema. In base alle informazioni fornite, il problema di prestazioni potrebbe essere correlato a un problema di memoria. Le prestazioni della tua applicazione in termini di interfaccia utente e memoria sono abbastanza correlate.

La scorsa settimana ho scoperto che la mia app perdeva memoria. L'ho scoperto perché dopo 20 minuti di utilizzo della mia app ho notato che l'interfaccia utente funzionava molto lentamente. La chiusura / apertura di un'attività o lo scorrimento di un RecyclerView con un gruppo di elementi è stato molto lento. Dopo aver monitorato alcuni dei miei utenti in produzione utilizzando http://flowup.io/ ho trovato questo:

inserisci qui la descrizione dell'immagine

Il tempo di frame era davvero molto alto e i frame al secondo davvero molto bassi. Puoi vedere che alcuni fotogrammi hanno richiesto circa 2 secondi per il rendering: S.

Cercando di capire cosa stesse causando questo cattivo tempo di frame / fps ho scoperto di avere un problema di memoria come puoi vedere qui:

inserisci qui la descrizione dell'immagine

Anche quando il consumo medio di memoria era vicino ai 15 MB, contemporaneamente l'app perdeva frame.

È così che ho scoperto il problema dell'interfaccia utente. Ho avuto una perdita di memoria nella mia app che causava molti eventi di garbage collector e questo stava causando le cattive prestazioni dell'interfaccia utente perché la VM Android ha dovuto interrompere la mia app per raccogliere memoria ogni singolo frame.

Guardando il codice ho riscontrato una perdita all'interno di una visualizzazione personalizzata perché non stavo annullando la registrazione di un ascoltatore dall'istanza di Android Choreographer. Dopo aver rilasciato la correzione, tutto è diventato normale :)

Se la tua app perde frame a causa di un problema di memoria, dovresti esaminare due errori comuni:

Verifica se la tua app sta allocando oggetti all'interno di un metodo richiamato più volte al secondo. Anche se questa allocazione può essere eseguita in un luogo diverso in cui l'applicazione sta diventando lenta. Un esempio potrebbe essere la creazione di nuove istanze di un oggetto all'interno di un metodo di visualizzazione personalizzato onDraw su onBindViewHolder nel titolare della visualizzazione del riciclatore. Verifica se la tua app sta registrando un'istanza in Android SDK ma non la rilascia. Anche la registrazione di un ascoltatore in un evento bus potrebbe essere una possibile perdita.

Dichiarazione di non responsabilità: lo strumento che ho utilizzato per monitorare la mia app è in fase di sviluppo. Ho accesso a questo strumento perché sono uno degli sviluppatori :) Se vuoi accedere a questo strumento, rilasceremo presto una versione beta! Puoi unirti al nostro sito web: http://flowup.io/ .

Se vuoi usare diversi strumenti puoi usare: traveview, dmtracedump, systrace o il monitor delle prestazioni Andorid integrato in Android Studio. Ma ricorda che questi strumenti monitoreranno il tuo dispositivo connesso e non il resto dei tuoi dispositivi utente o installazioni del sistema operativo Android.


3

È anche importante controllare il layout principale in cui si inserisce Recyclerview. Ho avuto problemi di scorrimento simili durante il test di recyclerView in un nestedscrollview. Una vista che scorre in un'altra vista che lo scorrimento può risentire delle prestazioni durante lo scorrimento


2

Nel mio caso, ho scoperto che la causa principale del ritardo è il frequente caricamento disegnabile all'interno del #onBindViewHolder()metodo. L'ho risolto semplicemente caricando le immagini come Bitmap una volta all'interno del ViewHolder e accedendovi dal metodo menzionato. Questo è tutto quello che ho fatto.


2

Nel mio RecyclerView, utilizzo le immagini bitmap per lo sfondo del mio item_layout.
Tutto ciò che ha detto @Galya è vero (e lo ringrazio per la sua ottima risposta). Ma non hanno funzionato per me.

Questo è ciò che ha risolto il mio problema:

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

Per ulteriori informazioni, leggi questa risposta .


2

Nel mio caso ho figli complessi con revisione del riciclaggio. Quindi ha influenzato il tempo di caricamento dell'attività (~ 5 sec per il rendering dell'attività)

Carico l'adattatore con postDelayed () -> questo darà un buon risultato per il rendering dell'attività. dopo aver eseguito il rendering del carico di riciclare con smooth.

Prova questa risposta,

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

1

Vedo nei commenti che stai già implementando il ViewHolderpattern, ma pubblicherò qui un adattatore di esempio che utilizza il RecyclerView.ViewHolderpattern in modo da poter verificare che lo stai integrando in modo simile, anche in questo caso il tuo costruttore può variare a seconda delle tue esigenze, qui è un esempio:

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 hai problemi a lavorare con RecyclerView.ViewHolderassicurati di avere le dipendenze appropriate che puoi verificare sempre su Gradle Please

Spero che risolva il tuo problema.


1

Questo mi ha aiutato a ottenere uno scorrimento più fluido:

eseguire l'override di onFailedToRecycleView (supporto ViewHolder) nell'adattatore

e interrompe tutte le animazioni in corso (se presenti). "animateview" .clearAnimation ();

ricordati di restituire vero;


1

L'ho risolto con questa riga di codice

recyclerView.setNestedScrollingEnabled(false);

1

Aggiungendo alla risposta di @ Galya, in bind viewHolder, stavo usando il metodo Html.fromHtml (). apparentemente questo ha un impatto sulle prestazioni.


0

Risolvi questo problema utilizzando l'unica riga della libreria Picasso

.in 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();
                        }
                    });
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.