Jerarquías de vista 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 pueden surgir.

Diseño y medición del rendimiento

La canalización de renderizado 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 la visualización o el 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 a fin de mantener 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 procese un fotograma dentro de los 16 ms permitidos, de modo que se pierde, y que se bloquee la animación.

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

Cómo administrar la complejidad: los diseños son importantes

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.

El caso más frecuente en que el diseño tarda más de lo habitual ocurre cuando las jerarquías de los objetos View están anidadas entre sí. 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.

Si usas la clase RelativeLayout, puedes lograr el mismo efecto, a menor costo, utilizando vistas LinearLayout anidadas y no ponderadas. Además, si tu app se orienta a Android 7.0 (API nivel 24), es probable que puedas usar un editor de diseño especial para crear un objeto ConstraintLayout en lugar de RelativeLayout. De esta manera, puedes evitar muchos de los problemas que se describen en esta sección. La clase ConstraintLayout ofrece un control de diseño similar, pero con un rendimiento mejorado. Esta clase usa su propio sistema de resolución de limitaciones para resolver relaciones entre vistas de una manera muy diferente a los diseños estándar.

Tributación doble

Por lo general, el marco de trabajo ejecuta la etapa de diseño o medición en un solo pase y de manera bastante rápida. Sin embargo, con los casos de diseño más complicados, es posible que el marco de trabajo 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 marco de trabajo realiza las siguientes acciones:

  1. Ejecuta un pase de diseño y medición. Durante este pase, el marco de trabajo 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 también 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 renderizado.

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

Los contenedores que no sean de RelativeLayout también podrían dar lugar a 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 marco de trabajo quizá necesite realizar un segundo pase para resolver los tamaños adecuados de los objetos.
  • La clase GridLayout tiene un problema similar. Aunque este contenedor 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 marco de trabajo deba realizar varios pases cuando el contenedor sea un RelativeLayout.

Los pases de diseño y medición múltiples no son, en sí mismos, un obstáculo para el rendimiento. Sin embargo, pueden serlo 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. Existen algunas herramientas que pueden brindarte indicaciones confiables sobre el lugar en el que ocurren los cuellos de botella de rendimiento. Otras herramientas ofrecen información menos precisa, pero también pueden brindarte sugerencias útiles.

Systrace

Una herramienta integrada en el SDK de Android que proporciona excelentes datos sobre el rendimiento es Systrace. Permite recopilar y analizar la información sobre el tiempo en todo el dispositivo Android a fin de que puedas ver cuándo los problemas de rendimiento de diseño causan problemas generales. Si deseas obtener más información sobre Systrace, consulta la descripción general del registro del sistema.

Profile GPU rendering

Otra herramienta que seguramente te proporcione información concreta sobre los cuellos de botella de rendimiento es Profile GPU rendering, integrada y disponible en dispositivos con Android 6.0 (API nivel 23) y versiones posteriores. Esta herramienta te permite ver cuánto dura la etapa de diseño y medición de cada fotograma de renderizado. 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, si hubiera alguno.

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 la explicación sobre 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: Ubicación de 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, puedes hacer clic en cada elemento a fin de expandirlo y acceder a información adicional en el panel de la derecha de la pantalla. En la figura 2, se muestra un ejemplo de esta pantalla.

Figura 2: Información de visualización sobre problemas específicos que identificó la herramienta Lint

Cuando haces clic en uno de estos elementos, se muestra, en el panel de la derecha, el problema asociado con ese elemento.

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

Layout Inspector

La herramienta Layout Inspector 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 principales de una vista en particular. Además, te permite inspeccionar los diseños que construye tu app.

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

Para obtener más información, consulta Cómo depurar tu diseño con Layout Inspector.

Cómo resolver los problemas de jerarquía

El concepto fundamental para solucionar problemas de rendimiento que surgen de las jerarquías de vistas es simple, pero resulta más complicado ponerlo en práctica. Evitar que las jerarquías de vistas impongan penalidades de rendimiento implica, por un lado, aplanar tu jerarquía de vistas y, por el otro, reducir la tributación doble. En esta sección, se presentan algunas estrategias para cumplir estos objetivos.

Cómo quitar diseños anidados redundantes

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 genera redundancia y agrega costos innecesarios a la jerarquía de vistas.

A menudo, Lint puede marcar este problema y reducir el tiempo de depuración.

Cómo fusionar o incluir diseños

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>
    

Y, luego, usar una etiqueta "include" para agregar este elemento al contenedor principal:

    <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 etiqueta <include> anida innecesariamente el primer diseño dentro del segundo.

La etiqueta merge puede ayudar a evitar este problema. Si deseas obtener más información sobre esta etiqueta, consulta Cómo volver a usar diseños con <include>.

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, quizás descubras que un TableLayout ofrece la misma funcionalidad que un diseño más complejo con muchas dependencias posicionales. En la versión N de Android, la clase ConstraintLayout ofrece funcionalidades similares a las de RelativeLayout, pero con un costo significativamente menor.