Rendimiento de desplazamiento de Android RecyclerView

86

He creado un ejemplo de RecyclerView basándome en la guía Creación de listas y tarjetas . Mi adaptador tiene una implementación de patrón solo para inflar el diseño.

El problema es el rendimiento deficiente del desplazamiento. Esto en un RecycleView con solo 8 elementos.

En algunas pruebas verifiqué que en Android L no ocurre este problema. Pero en la versión KitKat, la disminución del rendimiento es evidente.

falvojr
fuente
1
Intente utilizar el patrón de diseño ViewHolder para el rendimiento de desplazamiento: developer.android.com/training/improving-layouts/…
Haresh Chhelana
@HareshChhelana ¡gracias por tu respuesta! Pero ya estoy usando el patrón ViewHolder, según el enlace: developer.android.com/training/material/lists-cards.html
falvojr
2
¿Puede compartir algún código sobre la configuración de su adaptador y el archivo XML para sus diseños? Esto no parece normal. Además, ¿perfiló y vio dónde se gasta el tiempo?
yigit
2
Estoy enfrentando casi los mismos problemas. Excepto su rápida en pre Lollipop e increíble (muy) lenta en Android L.
Servus7
1
también puede compartir la versión de la biblioteca que está importando.
Droidekas

Respuestas:

227

Recientemente me enfrenté al mismo problema, así que esto es lo que hice con la última biblioteca de soporte de RecyclerView:

  1. Reemplace un diseño complejo (vistas anidadas, RelativeLayout) con el nuevo ConstraintLayout optimizado. Actívelo en Android Studio: Vaya a SDK Manager -> pestaña SDK Tools -> Support Repository -> marque ConstraintLayout para Android y Solver para ConstraintLayout. Agregue a las dependencias:

    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    
  2. Si es posible, haga que todos los elementos de RecyclerView tengan la misma altura . Y añadir:

    recyclerView.setHasFixedSize(true);
    
  3. Utilice los métodos predeterminados de caché de dibujo de RecyclerView y ajústelos según su caso. No necesita una biblioteca de terceros para hacerlo:

    recyclerView.setItemViewCacheSize(20);
    recyclerView.setDrawingCacheEnabled(true);
    recyclerView.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);
    
  4. Si usa muchas imágenes , asegúrese de que su tamaño y compresión sean óptimos . El escalado de imágenes también puede afectar el rendimiento. Hay dos lados del problema: la imagen de origen utilizada y el mapa de bits decodificado. El siguiente ejemplo le da una pista de cómo decodificar una imagen, descargada de la 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 más importante es especificar inPreferredConfig: define cuántos bytes se utilizarán para cada píxel de la imagen. Tenga en cuenta que esta es una opción preferida . Si la imagen de origen tiene más colores, aún se decodificará con una configuración diferente.

  1. Asegúrese de que onBindViewHolder () sea ​​lo más barato posible. Puede configurar OnClickListener una vez onCreateViewHolder()y llamar a través de una interfaz a un oyente fuera del Adaptador, pasando el elemento seleccionado. De esta manera, no crea objetos adicionales todo el tiempo. También verifique las banderas y los estados, antes de realizar cambios en la vista aquí.

    viewHolder.itemView.setOnClickListener(new View.OnClickListener() {
          @Override
          public void onClick(View view) {
              Item item = getItem(getAdapterPosition());
              outsideClickListener.onItemClicked(item);
          }
    });
    
  2. Cuando se modifiquen los datos, intente actualizar solo los elementos afectados . Por ejemplo, en lugar de invalidar todo el conjunto de datos con notifyDataSetChanged(), al agregar / cargar más elementos, simplemente use:

    adapter.notifyItemRangeInserted(rangeStart, rangeEnd);
    adapter.notifyItemRemoved(position);
    adapter.notifyItemChanged(position);
    adapter.notifyItemInserted(position);
    
  3. Desde el sitio web para desarrolladores de Android :

Confíe en notifyDataSetChanged () como último recurso.

Pero si necesita usarlo, mantenga sus artículos con identificadores únicos :

    adapter.setHasStableIds(true);

RecyclerView intentará sintetizar eventos de cambio estructural visibles para los adaptadores que informan que tienen ID estables cuando se usa este método. Esto puede ayudar a los fines de la animación y la persistencia de objetos visuales, pero las vistas de elementos individuales aún deberán recuperarse y volverse a activar.

Incluso si hace todo bien, lo más probable es que RecyclerView todavía no funcione tan bien como le gustaría.

Galya
fuente
18
un voto para adapter.setHasStableIds (verdadero); método que realmente ayudó a hacer que la vista de reciclaje sea más rápida.
Atula
1
¡La parte 7 está totalmente equivocada! setHasStableIds (true) no hará nada excepto que use adapter.notifyDataSetChanged (). Enlace: developer.android.com/reference/android/support/v7/widget/…
localhost
1
Puedo ver por qué recyclerView.setItemViewCacheSize(20);pueden mejorar el rendimiento. ¡Pero recyclerView.setDrawingCacheEnabled(true);y recyclerView.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);aunque! No estoy seguro de que esto cambie algo. Estas son Viewllamadas específicas que le permiten recuperar mediante programación el caché de dibujo como un mapa de bits y utilizarlo para su ventaja más adelante. RecyclerViewno parece hacer nada al respecto.
Abdelhakim AKODADI
1
@AbdelhakimAkodadi, el desplazamiento se vuelve fluido con el caché. Lo he probado. ¿Cómo puedes decir lo contrario? Es obvio. Por supuesto, si alguien se desplaza como un loco, nada ayudará. Solo muestro las otras opciones como setDrawingCacheQuality, que no uso, porque la calidad de imagen es importante en mi caso. No predico DRAWING_CACHE_QUALI‌ TY_HIGH, pero sugiero a quien esté interesado que profundice y modifique la opción.
Galya
2
setDrawingCacheEnabled () y setDrawingCacheQuality () están en desuso. En su lugar, utilice la aceleración por hardware. developer.android.com/reference/android/view/…
Shayan_Aryan
14

Resolví este problema agregando la siguiente bandera:

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

waclaw
fuente
2
Esto supone una ligera mejora de rendimiento para mí. Pero RecyclerView sigue siendo muy lento, mucho más lento que un ListView personalizado equivalente.
SMBiggs
Ahh, pero descubrí por qué mi código es tan lento, no tiene nada que ver con setHasStableIds (). Publicaré una respuesta con más información.
SMBiggs
13

Descubrí al menos un patrón que puede matar tu desempeño. Recuerda que onBindViewHolder()se llama con frecuencia . Entonces, cualquier cosa que haga en ese código tiene el potencial de detener su desempeño. Si su RecyclerView hace alguna personalización, es muy fácil poner accidentalmente un código lento en este método.

Estaba cambiando las imágenes de fondo de cada RecyclerView según la posición. Pero cargar imágenes requiere un poco de trabajo, lo que hace que mi RecyclerView sea lento y desigual.

La creación de un caché para las imágenes funcionó de maravilla; onBindViewHolder()ahora solo modifica una referencia a una imagen en caché en lugar de cargarla desde cero. Ahora el RecyclerView avanza rápidamente.

Sé que no todos tendrán este problema exacto, por lo que no me molesto en cargar el código. Pero considere cualquier trabajo que se realice en usted onBindViewHolder()como un posible cuello de botella para un rendimiento deficiente de RecyclerView.

SMBiggs
fuente
Estoy teniendo el mismo problema. En este momento estoy usando Fresco para la carga y el almacenamiento en caché de imágenes. ¿Tiene otra solución mejor para cargar y almacenar en caché imágenes dentro de RecyclerView. Gracias.
Androidicus
No estoy familiarizado con Fresco (leyendo ... sus promesas son bonitas). Tal vez tengan algunas ideas sobre cómo usar mejor sus cachés con RecyclerViews. Y veo que no eres el único con este problema: github.com/facebook/fresco/issues/414 .
SMBiggs
10

Además de la respuesta detallada de @ Galya, quiero afirmar que, aunque puede ser un problema de optimización, también es cierto que tener el depurador habilitado puede ralentizar mucho las cosas.

Si hace todo lo posible para optimizar su RecyclerViewy aún no funciona correctamente, intente cambiar su variante de compilación a releasey verifique cómo funciona en un entorno que no es de desarrollo (con el depurador deshabilitado).

Me pasó que mi aplicación funcionaba lentamente en la debugvariante de compilación, pero tan pronto como cambié a la releasevariante, funcionó sin problemas. Esto no significa que deba desarrollar con la releasevariante de compilación, pero es bueno saber que siempre que esté listo para enviar su aplicación, funcionará bien.

blastervla
fuente
¡Este comentario fue muy útil para mí! He intentado todo para mejorar el rendimiento de mi RecyclerView, pero nada ayudó realmente, pero una vez que cambié a la versión de lanzamiento, me di cuenta de que todo estaba bien.
Tal Barda
1
Enfrenté el mismo problema incluso sin el depurador adjunto, pero tan pronto como se movió a la versión de lanzamiento, el problema ya no estaba a la vista
Farmaan Elahi
8

Tuve una charla sobre RecyclerViewsu desempeño. Aquí hay diapositivas en inglés y video grabado en ruso .

Contiene un conjunto de técnicas (algunas de ellas ya están cubiertas por la respuesta de @ Darya ).

Aquí hay un breve resumen:

  • Si los Adapterartículos tienen un tamaño fijo, configure:
    recyclerView.setHasFixedSize(true);

  • Si las entidades de datos pueden ser representadas por long ( hashCode()por ejemplo), entonces configure:
    adapter.hasStableIds(true);
    e implemente:
    // YourAdapter.java
    @Override
    public long getItemId(int position) {
    return items.get(position).hashcode(); //id()
    }
    En este caso Item.id()no funcionaría, porque permanecería igual incluso si Itemel contenido ha cambiado.
    PD ¡Esto no es necesario si está utilizando DiffUtil!

  • Utilice mapa de bits correctamente escalado. No reinvente la rueda y use bibliotecas.
    Más información sobre cómo elegir aquí .

  • Utilice siempre la última versión de RecyclerView. Por ejemplo, hubo enormes mejoras de rendimiento en la 25.1.0captación previa.
    Más info aquí .

  • Utilice DiffUtill.
    DiffUtil es imprescindible .
    Documentación oficial .

  • ¡Simplifique el diseño de su artículo!
    Pequeña biblioteca para enriquecer TextViews - TextViewRichDrawable

Consulte las diapositivas para obtener una explicación más detallada.

Oleksandr
fuente
7

No estoy realmente seguro de si el uso de la setHasStableIdbandera solucionará su problema. Según la información que proporcione, su problema de rendimiento podría estar relacionado con un problema de memoria. El rendimiento de su aplicación en términos de interfaz de usuario y memoria está bastante relacionado.

La semana pasada descubrí que mi aplicación estaba perdiendo memoria. Descubrí esto porque después de 20 minutos usando mi aplicación noté que la interfaz de usuario funcionaba muy lento. Cerrar / abrir una actividad o desplazarse por un RecyclerView con un montón de elementos fue realmente lento. Después de monitorear a algunos de mis usuarios en producción usando http://flowup.io/ encontré esto:

ingrese la descripción de la imagen aquí

El tiempo de fotogramas fue realmente muy alto y los fotogramas por segundo realmente muy bajos. Puede ver que algunos fotogramas necesitan unos 2 segundos para renderizarse: S.

Tratando de averiguar qué estaba causando este mal tiempo de fotograma / fps, descubrí que tenía un problema de memoria como puede ver aquí:

ingrese la descripción de la imagen aquí

Incluso cuando el consumo medio de memoria estaba cerca de los 15 MB al mismo tiempo, la aplicación estaba perdiendo fotogramas.

Así fue como descubrí el problema de la interfaz de usuario. Tuve una pérdida de memoria en mi aplicación que causó muchos eventos de recolección de basura y eso fue lo que causó el mal rendimiento de la interfaz de usuario porque la máquina virtual de Android tuvo que detener mi aplicación para recopilar memoria en cada fotograma.

Al mirar el código, tuve una fuga dentro de una vista personalizada porque no estaba anulando el registro de un oyente de la instancia de Android Choreographer. Después de lanzar la solución, todo se volvió normal :)

Si su aplicación está perdiendo fotogramas debido a un problema de memoria, debe revisar dos errores comunes:

Revise si su aplicación asigna objetos dentro de un método invocado varias veces por segundo. Incluso si esta asignación se puede realizar en un lugar diferente donde su aplicación se está volviendo lenta. Un ejemplo podría ser la creación de nuevas instancias de un objeto dentro de un método de vista personalizado onDraw en onBindViewHolder en su titular de vista de vista de reciclador. Revise si su aplicación está registrando una instancia en el SDK de Android pero no la está lanzando. El registro de un oyente en un evento de bus también podría ser una posible filtración.

Descargo de responsabilidad: la herramienta que he estado usando para monitorear mi aplicación está en desarrollo. Tengo acceso a esta herramienta porque soy uno de los desarrolladores :) Si quieres acceder a esta herramienta, pronto lanzaremos una versión beta. Puede unirse a nuestro sitio web: http://flowup.io/ .

Si quieres utilizar diferentes herramientas puedes utilizar: traveview, dmtracedump, systrace o el monitor de rendimiento de Andorid integrado en Android Studio. Pero recuerde que estas herramientas monitorearán su dispositivo conectado y no el resto de sus dispositivos de usuario o instalaciones del sistema operativo Android.

Pedro Vicente Gómez Sánchez
fuente
3

También es importante verificar el diseño principal en el que colocó su Recyclerview. Tuve problemas de desplazamiento similares cuando probé ReciclerView en una vista de desplazamiento anidada. Una vista que se desplaza en otra vista que el desplazamiento puede sufrir en el rendimiento durante el desplazamiento

Saintjab
fuente
Sí, punto válido. ¡Yo también enfrento el mismo problema!
Ranjith Kumar
2

En mi caso, descubrí que la causa notable del retraso es el #onBindViewHolder()método de carga interna de extracción frecuente . Lo resolví simplemente cargando las imágenes como Bitmap una vez dentro de ViewHolder y accediendo a él desde el método mencionado. Eso es todo lo que hice.

Wei Chan
fuente
2

En mi RecyclerView, utilizo imágenes de mapa de bits para el fondo de mi item_layout.
Todo lo que dijo @Galya es cierto (y le agradezco su gran respuesta). Pero no funcionaron para mí.

Esto es lo que resolvió mi problema:

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

Para obtener más información, lea esta respuesta .

mohandesR
fuente
2

En mi caso, tengo hijos complejos de reciclaje. Entonces afectó el tiempo de carga de la actividad (~ 5 segundos para la representación de la actividad)

Cargué el adaptador con postDelayed () -> esto dará un buen resultado para la representación de la actividad. después de la actividad renderizando mi carga de reciclaje con suavidad.

Prueba esta respuesta

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

Veo en los comentarios que ya estás implementando el ViewHolderpatrón, pero aquí publicaré un adaptador de ejemplo que usa el RecyclerView.ViewHolderpatrón para que puedas verificar que lo estás integrando de manera similar, nuevamente tu constructor puede variar dependiendo de tus necesidades, aquí es un ejemplo:

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

Si tiene algún problema para trabajar, RecyclerView.ViewHolderasegúrese de tener las dependencias adecuadas que puede verificar siempre en Gradle.

Espero que resuelva tu problema.

Joel
fuente
1

Esto me ayudó a obtener un desplazamiento más suave:

anular el onFailedToRecycleView (soporte de ViewHolder) en el adaptador

y detener cualquier titular de animaciones en curso (si corresponde). "animateview" .clearAnimation ();

recuerde devolver la verdad;

Rugido Grønmo
fuente
1

Lo resolví con esta línea de código

recyclerView.setNestedScrollingEnabled(false);
eli
fuente
1

Agregando a la respuesta de @ Galya, en bind viewHolder, estaba usando el método Html.fromHtml (). aparentemente esto tiene un impacto en el rendimiento.

Sami Adam
fuente
0

i Resuelva este problema utilizando la única línea de la biblioteca de Picasso

.ajuste()

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 Independientemente
fuente