Jerarquías vistas y rendimiento

La manera en la que administras la jerarquía de tus objetos View puede tener un impacto significativo en el rendimiento de tu app. En esta página, se describe cómo evaluar si tu jerarquía de vistas está haciendo que tu app sea más lenta. Además, se ofrecen algunas estrategias para abordar los problemas que podrían surgir.

Esta página se enfoca en mejorar los diseños basados en View. Si quieres obtener información para mejorar el rendimiento de Jetpack Compose, consulta Rendimiento de Jetpack Compose.

Diseño y medición del rendimiento

La canalización de renderización incluye una etapa de diseño y medición. Durante esta etapa, el sistema coloca de manera apropiada los elementos relevantes en tu jerarquía de vistas. La parte de medición de esta etapa determina los tamaños y los límites de los objetos View, mientras que la de diseño define en qué lugar de la pantalla se colocan los objetos View.

Las dos etapas de esta canalización incurren en costos menores por vista o diseño que procesan. En la mayoría de los casos, este costo es mínimo y no afecta el rendimiento de forma notoria. Sin embargo, el costo puede ser mayor si una app agrega o quita objetos View, como cuando un objeto RecyclerView los recicla o reutiliza. El costo también puede ser superior si un objeto View debe tener en cuenta un cambio de tamaño para cumplir con sus limitaciones. Por ejemplo, si tu app llama a SetText() en un objeto View que ajusta texto, puede ser necesario cambiar el tamaño de View.

Si los casos como este tardan demasiado, pueden impedir que se renderice un fotograma dentro de los 16 ms permitidos, lo que puede causar que los fotogramas se pierdan, y que se bloquee la animación.

Como no puedes mover estas operaciones a un subproceso de trabajo, tu app debe procesarlas en el subproceso principal. Lo más conveniente es optimizarlas para que tarden el menor tiempo posible.

Cómo administrar diseños complejos

Los diseños de Android te permiten anidar objetos de IU en la jerarquía de vistas. Este anidado también puede implicar un costo de diseño. Cuando tu app procesa el diseño de un objeto, también realiza el mismo proceso en todos los elementos secundarios del diseño.

Cuando un diseño es complicado, es posible que el costo surja solo la primera vez que el sistema lo compute. Por ejemplo, cuando tu app recicla un elemento de lista complejo en un objeto RecyclerView, el sistema necesita diseñar todos los objetos. En otro ejemplo, los cambios triviales pueden propagarse por la cadena hacia el elemento superior hasta llegar a un objeto que no afecte el tamaño del elemento superior.

Las jerarquías de los objetos View anidadas entre sí son un motivo frecuente por el que el diseño tarda más de lo habitual. Cada objeto de diseño anidado aumenta el costo de la etapa de diseño. Mientras más plana sea tu jerarquía, menos tiempo tardará en completarse la etapa de diseño.

Te recomendamos que uses el editor de diseño para crear un objeto ConstraintLayout, en lugar de RelativeLayout o LinearLayout, ya que, en general, es más eficiente y reduce el anidado de los diseños. Sin embargo, para diseños simples que se pueden lograr con FrameLayout, te recomendamos que uses FrameLayout.

Si usas la clase RelativeLayout, podrías lograr el mismo efecto, a menor costo, utilizando vistas LinearLayout anidadas y no ponderadas. Sin embargo, si usas vistas LinearLayout anidadas y ponderadas, el costo del diseño es mucho mayor porque requiere que el sistema realice varios pases de diseño, como se explica en la siguiente sección.

También te recomendamos que uses RecyclerView en lugar de ListView, ya que puede reciclar los diseños de elementos individuales de lista, lo que es más eficiente y puede mejorar el rendimiento del desplazamiento.

Tributación doble

Por lo general, el framework ejecuta la etapa de diseño o medición en un solo pase. Sin embargo, con los casos de diseño más complicados, es posible que el framework tenga que iterarse varias veces en partes de la jerarquía que requieren varios pases para la resolución antes de colocar los elementos de forma definitiva. Se conoce como tributación doble al hecho de tener que realizar más de una iteración de diseño y medición.

Por ejemplo, cuando usas el contenedor RelativeLayout, que te permite colocar objetos View según las posiciones de otros objetos View, el framework realiza las siguientes secuencias:

  1. Ejecuta un pase de diseño y medición. Durante este pase, el framework calcula la posición y el tamaño de cada objeto secundario, según la solicitud de cada uno de ellos.
  2. Usa estos datos (y tiene en cuenta el peso de los objetos) para determinar la ubicación apropiada de las vistas correlacionadas.
  3. Realiza un segundo pase de diseño para finalizar las posiciones de los objetos.
  4. Pasa a la siguiente etapa del proceso de renderización.

Mientras más niveles tenga tu jerarquía de vistas, mayor será la penalidad de rendimiento potencial.

Como se mencionó anteriormente, ConstraintLayout suele ser más eficiente que otros diseños, excepto FrameLayout. Es menos propenso a varios pases de diseño y, en muchos casos, elimina la necesidad de anidar diseños.

Los contenedores que no sean de RelativeLayout también pueden aumentar la tributación doble. Por ejemplo:

  • Una vista de LinearLayout horizontal podría generar un pase de diseño y medición doble. Un pase de diseño y medición doble también puede ocurrir en una orientación vertical si agregas measureWithLargestChild. En este caso, el framework quizá necesite realizar un segundo pase para resolver los tamaños adecuados de los objetos.
  • GridLayout también permite el posicionamiento relativo, normalmente evita la tributación doble, ya que procesa con antelación las relaciones posicionales entre las vistas secundarias. Sin embargo, si el diseño usa pesos o se completa con la clase Gravity, se pierde el beneficio de ese procesamiento previo, y es posible que el framework deba realizar varios pases cuando el contenedor sea un RelativeLayout.

Los pases de diseño y medición múltiples no son, necesariamente, un obstáculo para el rendimiento. Sin embargo, pueden convertirse en uno si están en el lugar equivocado. Debes tener cuidado cuando una de las siguientes condiciones se aplique a tu contenedor:

  • Es un elemento raíz en tu jerarquía de vistas.
  • Tiene una jerarquía de vistas profunda debajo de él.
  • Hay muchas instancias del contenedor llenando la pantalla, de un modo similar a los elementos secundarios en un objeto ListView.

Cómo diagnosticar problemas de jerarquía de vistas

El rendimiento del diseño es un problema complejo con varias facetas. Las siguientes herramientas pueden ayudarte a identificar dónde ocurren los cuellos de botella de rendimiento. Algunas herramientas ofrecen información menos precisa, pero pueden brindarte sugerencias útiles.

Perfetto

Perfetto es una herramienta que proporciona datos sobre el rendimiento. Puedes abrir esos registros de Android en la IU de Perfetto.

Profile GPU rendering

La herramienta Profile GPU rendering, integrada y disponible en dispositivos con Android 6.0 (nivel de API 23) y versiones posteriores puede proporcionarte información concreta sobre los cuellos de botella de rendimiento. Esta herramienta te permite ver cuánto dura la etapa de diseño y medición de cada fotograma de renderización. Estos datos pueden ayudarte a diagnosticar problemas de rendimiento durante el tiempo de ejecución y a determinar cuáles son los problemas de diseño y medición que debes abordar.

En la representación gráfica de los datos que captura, la herramienta Profile GPU rendering usa el color azul para mostrar el tiempo de diseño. Si deseas obtener más información sobre cómo usar esta herramienta, consulta Velocidad de Profile GPU rendering.

Lint

La herramienta Lint de Android Studio puede ayudarte a tener una idea de las ineficiencias en la jerarquía de vistas. Para usar esta herramienta, selecciona Analyze > Inspect Code, como se muestra en la Figura 1.

Figura 1: Selecciona Inspect Code en Android Studio.

Información sobre los distintos elementos de diseño que aparecen en Android > Lint > Performance. Para ver más detalles, haz clic en cada elemento para expandirlo y mostrar información adicional en el panel de la derecha de la pantalla. En la Figura 2, se muestra un ejemplo de cómo se expande la información.

Figura 2: Visualización de la información sobre problemas específicos que identifica la herramienta Lint

Cuando haces clic en un elemento, se muestran los problemas asociados con ese elemento en el panel de la derecha.

Para obtener más información sobre temas y problemas específicos en esta área, consulta la documentación sobre Lint.

Inspector de diseño

La herramienta Inspector de diseño de Android Studio ofrece una representación visual de la jerarquía de vistas de tu app. Es una manera efectiva para navegar por la jerarquía de tu app y obtener una representación visual clara de la cadena de objetos superiores de una vista en particular. Además, te permite inspeccionar los diseños que construye tu app.

Las vistas que presenta Inspector de diseño también pueden ayudarte a identificar problemas de rendimiento que surgen a partir de la tributación doble. Además, puede brindarte una manera para identificar cadenas profundas de diseños anidados o áreas de diseño con grandes cantidades de elementos secundarios anidados, que pueden ser una fuente de costos de rendimiento. En estos casos, las etapas de diseño y medición pueden ser costosas, lo que podría generar problemas de rendimiento.

Para obtener más información, consulta Cómo depurar tu diseño con el Inspector de diseño y la validación de diseño.

Cómo resolver los problemas de jerarquía de vistas

El concepto fundamental para solucionar problemas de rendimiento que surgen de las jerarquías de vistas puede resultar complicado ponerlo en práctica. Evitar que las jerarquías de vistas impongan penalizaciones de rendimiento consiste en aplanar tu jerarquía de vistas y reducir la doble imposición. En esta sección, se presentan estrategias para cumplir estos objetivos.

Cómo quitar diseños anidados redundantes

ConstraintLayout es una biblioteca de Jetpack con una gran cantidad de mecanismos diferentes para posicionar vistas dentro del diseño. De esa manera, se reduce la necesidad de anidar un elemento ConstaintLayout, y puede ayudar a aplanar la jerarquía de vistas. Por lo general, es más sencillo aplanar jerarquías con ConstraintLayout en comparación con otros tipos de diseño.

A menudo, los desarrolladores usan más diseños anidados de los necesarios. Por ejemplo, un contenedor RelativeLayout podría incluir un solo elemento secundario que también sea un contenedor RelativeLayout. Este anidado es redundante y agrega costos innecesarios a la jerarquía de vistas. Lint puede marcar este problema y reducir el tiempo de depuración.

Cómo adoptar merge o include

Una causa frecuente de diseños anidados redundantes es la etiqueta <include>. Por ejemplo, podrías definir un diseño reutilizable de la siguiente manera:

<LinearLayout>
    <!-- some stuff here -->
</LinearLayout>

Luego, puedes agregar una etiqueta <include> para agregar el siguiente elemento al contenedor superior:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/app_bg"
    android:gravity="center_horizontal">

    <include layout="@layout/titlebar"/>

    <TextView android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:text="@string/hello"
              android:padding="10dp" />

    ...

</LinearLayout>

La anterior etiqueta <include> anida innecesariamente el primer diseño dentro del segundo.

La etiqueta <merge> puede ayudar a evitar este problema. Para obtener información sobre esta etiqueta, consulta Usa la etiqueta <merge>.

Cómo usar un diseño más económico

Es posible que no puedas ajustar tu esquema de diseño existente para que excluya diseños redundantes. En algunos casos, la única solución puede consistir en aplanar tu jerarquía. Para ello, debes cambiar a un tipo de diseño completamente diferente.

Por ejemplo, es posible que TableLayout proporcione la misma funcionalidad que un diseño más complejo con muchas dependencias posicionales. La biblioteca de Jetpack ConstraintLayout proporciona una funcionalidad similar a RelativeLayout, además de más funciones para ayudar a crear diseños más planos y eficientes.