Consejos y herramientas para optimizar las aplicaciones de Android

Publicado: 2022-03-11

Los dispositivos Android tienen muchos núcleos, por lo que escribir aplicaciones fluidas es una tarea sencilla para cualquiera, ¿verdad? Incorrecto. Como todo en Android se puede hacer de muchas maneras diferentes, elegir la mejor opción puede ser difícil. Si desea elegir el método más eficiente, debe saber qué sucede debajo del capó. Afortunadamente, no tienes que depender de tus sentimientos o sentido del olfato, ya que existen muchas herramientas que pueden ayudarte a encontrar cuellos de botella midiendo y describiendo lo que está pasando. Las aplicaciones fluidas y correctamente optimizadas mejoran en gran medida la experiencia del usuario y también consumen menos batería.

Veamos primero algunos números para considerar cuán importante es realmente la optimización. Según una publicación de Nimbledroid, el 86 % de los usuarios (incluyéndome a mí) han desinstalado aplicaciones después de usarlas solo una vez debido al bajo rendimiento. Si está cargando algún contenido, tiene menos de 11 segundos para mostrárselo al usuario. Solo cada tercer usuario le dará más tiempo. También puede recibir muchas críticas negativas en Google Play debido a eso.

Cree mejores aplicaciones: patrones de rendimiento de Android

Poner a prueba la paciencia de sus usuarios es un atajo para la desinstalación.
Pío

Lo primero que notan todos los usuarios una y otra vez es el tiempo de inicio de la aplicación. Según otra publicación de Nimbledroid, de las 100 mejores aplicaciones, 40 comienzan en menos de 2 segundos y 70 comienzan en menos de 3 segundos. Entonces, si es posible, generalmente debe mostrar algún contenido lo antes posible y retrasar un poco las verificaciones de antecedentes y las actualizaciones.

Recuerde siempre, la optimización prematura es la raíz de todos los males. Tampoco debe perder demasiado tiempo con la microoptimización. Verá el mayor beneficio de optimizar el código que se ejecuta con frecuencia. Por ejemplo, esto incluye la función onDraw() , que ejecuta cada cuadro, idealmente 60 veces por segundo. Dibujar es la operación más lenta que existe, así que intente volver a dibujar solo lo que sea necesario. Más sobre esto vendrá más adelante.

Consejos de rendimiento

Suficiente teoría, aquí hay una lista de algunas de las cosas que debe considerar si el rendimiento es importante para usted.

1. Cadena frente a StringBuilder

Digamos que tiene una cadena y, por alguna razón, desea agregarle más cadenas 10 mil veces. El código podría ser algo como esto.

 String string = "hello"; for (int i = 0; i < 10000; i++) { string += " world"; }

Puede ver en los monitores de Android Studio cuán ineficientes pueden ser algunas concatenaciones de cadenas. Hay toneladas de recolección de basura (GC) en curso.

Cadena vs StringBuilder

Esta operación toma alrededor de 8 segundos en mi dispositivo bastante bueno, que tiene Android 5.1.1. La forma más eficiente de lograr el mismo objetivo es usar un StringBuilder, como este.

 StringBuilder sb = new StringBuilder("hello"); for (int i = 0; i < 10000; i++) { sb.append(" world"); } String string = sb.toString();

En el mismo dispositivo esto sucede casi instantáneamente, en menos de 5ms. Las visualizaciones de CPU y memoria son casi totalmente planas, por lo que puede imaginar cuán grande es esta mejora. Tenga en cuenta, sin embargo, que para lograr esta diferencia, tuvimos que agregar 10 mil cadenas, lo que probablemente no haga con frecuencia. Entonces, en caso de que agregue solo un par de cadenas una vez, no verá ninguna mejora. Por cierto, si lo haces:

 String string = "hello" + " world";

Se convierte internamente en un StringBuilder, por lo que funcionará bien.

Quizás se esté preguntando, ¿por qué la concatenación de cadenas de la primera forma es tan lenta? Es causado por el hecho de que las cadenas son inmutables, por lo que una vez que se crean, no se pueden cambiar. Incluso si cree que está cambiando el valor de una cadena, en realidad está creando una nueva cadena con el nuevo valor. En un ejemplo como:

 String myString = "hello"; myString += " world";

Lo que obtendrá en la memoria no es 1 cadena "hola mundo", sino 2 cadenas. El String myString contendrá "hola mundo", como era de esperar. Sin embargo, la cadena original con el valor "hola" todavía está viva, sin ninguna referencia a ella, a la espera de que se recopile la basura. Esta es también la razón por la que debe almacenar contraseñas en una matriz de caracteres en lugar de una cadena. Si almacena una contraseña como una cadena, permanecerá en la memoria en formato legible por humanos hasta el próximo GC durante un período de tiempo impredecible. Volviendo a la inmutabilidad descrita anteriormente, la cadena permanecerá en la memoria incluso si le asigna otro valor después de usarla. Sin embargo, si vacía la matriz de caracteres después de usar la contraseña, desaparecerá de todas partes.

2. Elegir el tipo de datos correcto

Antes de comenzar a escribir código, debe decidir qué tipos de datos usará para su colección. Por ejemplo, ¿debería usar un Vector o un ArrayList ? Bueno, depende de tu caso de uso. Si necesita una colección segura para subprocesos, que permitirá que solo un subproceso a la vez funcione con ella, debe elegir un Vector , ya que está sincronizado. En otros casos, probablemente debería apegarse a un ArrayList , a menos que realmente tenga una razón específica para usar vectores.

¿Qué tal el caso cuando quieres una colección con objetos únicos? Bueno, probablemente deberías elegir un Set . No pueden contener duplicados por diseño, por lo que no tendrás que encargarte tú mismo. Hay varios tipos de conjuntos, así que elija uno que se ajuste a su caso de uso. Para un grupo simple de elementos únicos, puede usar un HashSet . Si desea conservar el orden de los elementos en los que se insertaron, elija un LinkedHashSet . Un TreeSet clasifica los elementos automáticamente, por lo que no tendrá que llamar a ningún método de clasificación. También debería ordenar los elementos de manera eficiente, sin tener que pensar en algoritmos de clasificación.

Los datos dominan. Si ha elegido las estructuras de datos correctas y ha organizado bien las cosas, los algoritmos casi siempre serán evidentes. Las estructuras de datos, no los algoritmos, son fundamentales para la programación.
— Las 5 reglas de programación de Rob Pike

Ordenar números enteros o cadenas es bastante sencillo. Sin embargo, ¿qué sucede si desea ordenar una clase por alguna propiedad? Supongamos que está escribiendo una lista de las comidas que come y almacena sus nombres y marcas de tiempo. ¿Cómo ordenaría las comidas por marca de tiempo de menor a mayor? Afortunadamente, es bastante simple. Es suficiente implementar la interfaz Comparable en la clase Meal y anular la función compareTo() . Para ordenar las comidas por marca de tiempo de menor a mayor, podríamos escribir algo como esto.

 @Override public int compareTo(Object object) { Meal meal = (Meal) object; if (this.timestamp < meal.getTimestamp()) { return -1; } else if (this.timestamp > meal.getTimestamp()) { return 1; } return 0; }

3. Actualizaciones de ubicación

Hay muchas aplicaciones que recopilan la ubicación del usuario. Debe usar la API de servicios de ubicación de Google para ese propósito, que contiene muchas funciones útiles. Hay un artículo separado sobre su uso, por lo que no lo repetiré.

Me gustaría enfatizar algunos puntos importantes desde la perspectiva del rendimiento.

En primer lugar, use solo la ubicación más precisa que necesite. Por ejemplo, si está haciendo un pronóstico del tiempo, no necesita la ubicación más precisa. Obtener solo un área muy aproximada basada en la red es más rápido y más eficiente con la batería. Puede lograrlo estableciendo la prioridad en LocationRequest.PRIORITY_LOW_POWER .

También puede usar una función de LocationRequest llamada setSmallestDisplacement() . Establecer esto en metros hará que su aplicación no sea notificada sobre el cambio de ubicación si fue menor que el valor dado. Por ejemplo, si tiene un mapa con restaurantes cercanos a su alrededor y establece el desplazamiento más pequeño en 20 metros, la aplicación no realizará solicitudes para verificar restaurantes si el usuario simplemente está caminando en una habitación. Las solicitudes serían inútiles, ya que de todos modos no habría ningún nuevo restaurante cercano.

La segunda regla es solicitar actualizaciones de ubicación solo con la frecuencia que las necesite. Esto se explica por sí mismo. Si realmente está creando esa aplicación de pronóstico del tiempo, no necesita solicitar la ubicación cada pocos segundos, ya que probablemente no tenga pronósticos tan precisos (contácteme si los tiene). Puede usar la función setInterval() para configurar el intervalo requerido en el que el dispositivo actualizará su aplicación sobre la ubicación. Si varias aplicaciones siguen solicitando la ubicación del usuario, todas las aplicaciones recibirán una notificación cada vez que se actualice una nueva ubicación, incluso si tiene establecido un setInterval() más alto. Para evitar que su aplicación reciba notificaciones con demasiada frecuencia, asegúrese de establecer siempre un intervalo de actualización más rápido con setFastestInterval() .

Y finalmente, la tercera regla es solicitar actualizaciones de ubicación solo si las necesita. Si está mostrando algunos objetos cercanos en el mapa cada x segundos y la aplicación se pone en segundo plano, no necesita conocer la nueva ubicación. No hay razón para actualizar el mapa si el usuario no puede verlo de todos modos. Asegúrate de dejar de escuchar las actualizaciones de ubicación cuando corresponda, preferiblemente en onPause() . Luego puede reanudar las actualizaciones en onResume() .

4. Solicitudes de red

Existe una alta probabilidad de que su aplicación esté utilizando Internet para descargar o cargar datos. Si es así, tiene varias razones para prestar atención al manejo de solicitudes de red. Uno de ellos son los datos móviles, que son muy limitados para muchas personas y no debes desperdiciarlos.

El segundo es la batería. Tanto el WiFi como las redes móviles pueden consumir bastante si se usan demasiado. Digamos que quieres descargar 1 kb. Para realizar una solicitud de red, debe activar la radio celular o WiFi, luego puede descargar sus datos. Sin embargo, la radio no se dormirá inmediatamente después de la operación. Permanecerá en un estado bastante activo durante unos 20-40 segundos más, dependiendo de su dispositivo y proveedor.

Solicitudes de red

¿Entonces, qué puede hacer usted al respecto? Lote. Para evitar que la radio se encienda cada dos segundos, recopile cosas que el usuario pueda necesitar en los próximos minutos. La forma adecuada de procesamiento por lotes es muy dinámica según su aplicación, pero si es posible, debe descargar los datos que el usuario pueda necesitar en los próximos 3 a 4 minutos. También se pueden editar los parámetros del lote según el tipo de Internet del usuario o el estado de carga. Por ejemplo, si el usuario está conectado a WiFi mientras se carga, puede obtener muchos más datos que si el usuario está conectado a Internet móvil con poca batería. Tener en cuenta todas estas variables puede ser algo difícil, algo que solo unas pocas personas harían. Afortunadamente, ¡GCM Network Manager está al rescate!

GCM Network Manager es una clase realmente útil con muchos atributos personalizables. Puede programar fácilmente tareas repetitivas y únicas. En las tareas repetitivas, puede establecer el intervalo de repetición más bajo, así como el más alto. Esto permitirá procesar por lotes no solo sus solicitudes, sino también las solicitudes de otras aplicaciones. La radio debe activarse solo una vez por período y, mientras está encendida, todas las aplicaciones en la cola descargan y cargan lo que se supone que deben hacer. Este administrador también conoce el tipo de red y el estado de carga del dispositivo, por lo que puede ajustarlo en consecuencia. Puede encontrar más detalles y muestras en este artículo, le insto a que lo revise. Una tarea de ejemplo se ve así:

 Task task = new OneoffTask.Builder() .setService(CustomService.class) .setExecutionWindow(0, 30) .setTag(LogService.TAG_TASK_ONEOFF_LOG) .setUpdateCurrent(false) .setRequiredNetwork(Task.NETWORK_STATE_CONNECTED) .setRequiresCharging(false) .build();

Por cierto, desde Android 3.0, si realiza una solicitud de red en el hilo principal, obtendrá una NetworkOnMainThreadException . Eso definitivamente te advertirá que no vuelvas a hacer eso.

5. Reflexión

La reflexión es la capacidad de las clases y los objetos para examinar sus propios constructores, campos, métodos, etc. Se usa generalmente para la compatibilidad con versiones anteriores, para verificar si un método determinado está disponible para una versión de sistema operativo en particular. Si tiene que usar la reflexión para ese propósito, asegúrese de almacenar en caché la respuesta, ya que usar la reflexión es bastante lento. Algunas bibliotecas ampliamente utilizadas también usan Reflection, como Roboguice para la inyección de dependencia. Esa es la razón por la que deberías preferir Dagger 2. Para obtener más detalles sobre la reflexión, puedes consultar una publicación separada.

6. Autoboxeo

Autoboxing y unboxing son procesos de conversión de un tipo primitivo a un tipo de objeto, o viceversa. En la práctica, significa convertir un int en un entero. Para lograr eso, el compilador usa la función Integer.valueOf() internamente. La conversión no solo es lenta, los objetos también requieren mucha más memoria que sus equivalentes primitivos. Veamos algo de código.

 Integer total = 0; for (int i = 0; i < 1000000; i++) { total += i; }

Si bien esto toma 500 ms en promedio, reescribirlo para evitar el autoboxing lo acelerará drásticamente.

 int total = 0; for (int i = 0; i < 1000000; i++) { total += i; }

Esta solución funciona a unos 2 ms, que es 25 veces más rápido. Si no me crees, pruébalo. Obviamente, los números serán diferentes por dispositivo, pero aún así debería ser mucho más rápido. Y también es un paso realmente simple para optimizar.

De acuerdo, probablemente no crees una variable de tipo Integer como esta a menudo. Pero, ¿qué pasa con los casos en que es más difícil de evitar? Como en un mapa, donde tienes que usar Objetos, como Map<Integer, Integer> ? Mira la solución que usa mucha gente.

 Map<Integer, Integer> myMap = new HashMap<>(); for (int i = 0; i < 100000; i++) { myMap.put(i, random.nextInt()); }

Insertar 100k entradas aleatorias en el mapa tarda alrededor de 250ms en ejecutarse. Ahora mire la solución con SparseIntArray.

 SparseIntArray myArray = new SparseIntArray(); for (int i = 0; i < 100000; i++) { myArray.put(i, random.nextInt()); }

Esto toma mucho menos, aproximadamente 50 ms. También es uno de los métodos más fáciles para mejorar el rendimiento, ya que no es necesario hacer nada complicado y el código sigue siendo legible. Mientras ejecutaba una aplicación clara con la primera solución, ocupaba 13 MB de mi memoria, usar ints primitivos ocupaba algo menos de 7 MB, por lo que solo la mitad.

SparseIntArray es solo una de las colecciones geniales que pueden ayudarlo a evitar el autoboxing. Un mapa como Map<Integer, Long> podría reemplazarse por SparseLongArray , ya que el valor del mapa es de tipo Long . Si observa el código fuente de SparseLongArray , verá algo bastante interesante. Debajo del capó, es básicamente solo un par de matrices. También puede usar SparseBooleanArray de manera similar.

Si lee el código fuente, es posible que haya notado una nota que dice que SparseIntArray puede ser más lento que HashMap . He estado experimentando mucho, pero para mí, SparseIntArray siempre fue mejor tanto en memoria como en rendimiento. Supongo que aún depende de usted cuál elija, experimente con sus casos de uso y vea cuál se adapta más a usted. Definitivamente tenga SparseArrays en su cabeza cuando use mapas.

7. En el sorteo

Como dije anteriormente, cuando está optimizando el rendimiento, probablemente verá el mayor beneficio en la optimización del código que se ejecuta con frecuencia. Una de las funciones que se ejecuta mucho es onDraw() . Puede que no te sorprenda que sea responsable de dibujar las vistas en la pantalla. Como los dispositivos suelen funcionar a 60 fps, la función se ejecuta 60 veces por segundo. Cada cuadro tiene 16 ms para ser completamente manejado, incluyendo su preparación y dibujo, por lo que deberías evitar las funciones lentas. Solo el hilo principal puede dibujar en la pantalla, por lo que debe evitar realizar operaciones costosas en él. Si congela el hilo principal durante varios segundos, es posible que obtenga el infame cuadro de diálogo Aplicación que no responde (ANR). Para cambiar el tamaño de las imágenes, el trabajo de la base de datos, etc., use un subproceso de fondo.

Si cree que sus usuarios no notarán esa caída en la velocidad de fotogramas, ¡está equivocado!
Pío

He visto a algunas personas tratando de acortar su código, pensando que será más eficiente de esa manera. Definitivamente ese no es el camino a seguir, ya que un código más corto no significa un código más rápido. Bajo ninguna circunstancia debe medir la calidad del código por el número de líneas.

Una de las cosas que debes evitar en onDraw() es asignar objetos como Paint. Prepare todo en el constructor, para que esté listo al dibujar. Incluso si tiene onDraw() optimizado, debe llamarlo solo con la frecuencia necesaria. ¿Qué es mejor que llamar a una función optimizada? Bueno, sin llamar a ninguna función en absoluto. En caso de que quieras dibujar texto, hay una función de ayuda bastante ordenada llamada drawText() , donde puedes especificar cosas como el texto, las coordenadas y el color del texto.

8. Soportes de vista

Probablemente conozcas este, pero no puedo saltármelo. El patrón de diseño Viewholder es una forma de suavizar las listas de desplazamiento. Es una especie de almacenamiento en caché de vistas, que puede reducir seriamente las llamadas a findViewById() e inflar las vistas almacenándolas. Puede verse algo como esto.

 static class ViewHolder { TextView title; TextView text; public ViewHolder(View view) { title = (TextView) view.findViewById(R.id.title); text = (TextView) view.findViewById(R.id.text); } }

Luego, dentro de la función getView() de su adaptador, puede verificar si tiene una vista utilizable. Si no, creas uno.

 ViewHolder viewHolder; if (convertView == null) { convertView = inflater.inflate(R.layout.list_item, viewGroup, false); viewHolder = new ViewHolder(convertView); convertView.setTag(viewHolder); } else { viewHolder = (ViewHolder) convertView.getTag(); } viewHolder.title.setText("Hello World");

Puede encontrar mucha información útil sobre este patrón en Internet. También se puede usar en los casos en que su vista de lista tiene varios tipos diferentes de elementos, como algunos encabezados de sección.

9. Cambiar el tamaño de las imágenes

Lo más probable es que su aplicación contenga algunas imágenes. En caso de que esté descargando algunos JPG de la web, pueden tener resoluciones realmente enormes. Sin embargo, los dispositivos en los que se mostrarán serán mucho más pequeños. Incluso si toma una foto con la cámara de su dispositivo, debe reducir su tamaño antes de mostrarla, ya que la resolución de la foto es mucho mayor que la resolución de la pantalla. Cambiar el tamaño de las imágenes antes de mostrarlas es algo crucial. Si intentara mostrarlos en resoluciones completas, se quedaría sin memoria bastante rápido. Se ha escrito mucho sobre la visualización eficiente de mapas de bits en los documentos de Android, intentaré resumirlo.

Así que tienes un mapa de bits, pero no sabes nada al respecto. Hay una bandera útil de mapas de bits llamada inJustDecodeBounds a su servicio, que le permite averiguar la resolución del mapa de bits. Supongamos que su mapa de bits es de 1024x768 y el ImageView utilizado para mostrarlo es de solo 400x300. Debe seguir dividiendo la resolución del mapa de bits por 2 hasta que sea aún más grande que el ImageView dado. Si lo hace, reducirá la muestra del mapa de bits en un factor de 2, lo que le dará un mapa de bits de 512x384. El mapa de bits reducido utiliza 4 veces menos memoria, lo que le ayudará mucho a evitar el famoso error OutOfMemory.

Ahora que sabes cómo hacerlo, no debes hacerlo. … Al menos, no si su aplicación se basa en gran medida en las imágenes, y no son solo 1-2 imágenes. Definitivamente evite cosas como cambiar el tamaño y reciclar imágenes manualmente, use algunas bibliotecas de terceros para eso. Los más populares son Picasso de Square, Universal Image Loader, Fresco de Facebook o mi favorito, Glide. Hay una gran comunidad activa de desarrolladores a su alrededor, por lo que también puede encontrar muchas personas útiles en la sección de problemas en GitHub.

10. Modo estricto

Strict Mode es una herramienta de desarrollo bastante útil que mucha gente no conoce. Por lo general, se usa para detectar solicitudes de red o accesos al disco desde el hilo principal. Puede establecer qué problemas debe buscar el modo estricto y qué penalización debe activar. Una muestra de Google se ve así:

 public void onCreate() { if (DEVELOPER_MODE) { StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() .detectDiskReads() .detectDiskWrites() .detectNetwork() .penaltyLog() .build()); StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() .detectLeakedSqlLiteObjects() .detectLeakedClosableObjects() .penaltyLog() .penaltyDeath() .build()); } super.onCreate(); }

Si desea detectar todos los problemas que puede encontrar el modo estricto, también puede usar detectAll() . Al igual que con muchos consejos de rendimiento, no debe intentar arreglar ciegamente todo lo que informa el Modo estricto. Solo investíguelo, y si está seguro de que no es un problema, déjelo en paz. También asegúrese de usar el modo estricto solo para la depuración y siempre desactívelo en las compilaciones de producción.

Rendimiento de depuración: la forma profesional

Veamos ahora algunas herramientas que pueden ayudarte a encontrar cuellos de botella, o al menos mostrar que algo anda mal.

1. Monitor Android

Esta es una herramienta integrada en Android Studio. De manera predeterminada, puede encontrar el Monitor de Android en la esquina inferior izquierda y puede cambiar entre 2 pestañas allí. Logcat y Monitores. La sección Monitores contiene 4 gráficos diferentes. Red, CPU, GPU y memoria. Se explican por sí mismos, así que los repasaré rápidamente. Aquí hay una captura de pantalla de los gráficos tomados mientras se analiza algo de JSON a medida que se descarga.

monitor androide

La parte Red muestra el tráfico entrante y saliente en KB/s. La parte de la CPU muestra el uso de la CPU en porcentaje. El monitor de la GPU muestra cuánto tiempo lleva renderizar los marcos de una ventana de la interfaz de usuario. Este es el monitor más detallado de estos 4, así que si quieres más detalles al respecto, lee esto.

Por último, tenemos el monitor de memoria, que probablemente usará más. De forma predeterminada, muestra la cantidad actual de memoria libre y asignada. También puede forzar una recolección de basura con él, para probar si la cantidad de memoria utilizada disminuye. Tiene una función útil llamada Dump Java Heap, que creará un archivo HPROF que se puede abrir con HPROF Viewer and Analyzer. Eso le permitirá ver cuántos objetos ha asignado, qué cantidad de memoria ocupa qué y tal vez qué objetos están causando pérdidas de memoria. Aprender a usar este analizador no es la tarea más sencilla que existe, pero vale la pena. Lo siguiente que puede hacer con el Monitor de memoria es realizar un Seguimiento de asignación cronometrado, que puede iniciar y detener como desee. Podría ser útil en muchos casos, por ejemplo, al desplazar o rotar el dispositivo.

2. Sobregiro de GPU

Esta es una herramienta de ayuda simple, que puede activar en Opciones de desarrollador una vez que haya habilitado el modo de desarrollador. Seleccione Depurar sobregiro de GPU, "Mostrar áreas de sobregiro", y su pantalla obtendrá algunos colores extraños. Está bien, eso es lo que se supone que debe pasar. Los colores indican cuántas veces se sobredibujó un área en particular. El color verdadero significa que no hubo sobregiro, esto es a lo que debe apuntar. Azul significa un sobregiro, verde significa dos, rosa tres, rojo cuatro.

Sobregiro de GPU

Si bien ver el color verdadero es lo mejor, siempre verá algunos sobredibujados, especialmente alrededor de los textos, los cajones de navegación, los diálogos y más. Así que no intentes deshacerte de él por completo. Si su aplicación es azulada o verdosa, probablemente esté bien. Sin embargo, si ve demasiado rojo en algunas pantallas simples, debe investigar qué está pasando. Es posible que haya demasiados fragmentos apilados unos sobre otros, si continúa agregándolos en lugar de reemplazarlos. Como mencioné anteriormente, dibujar es la parte más lenta de las aplicaciones, por lo que no tiene sentido dibujar algo si habrá más de 3 capas dibujadas en él. Siéntase libre de ver sus aplicaciones favoritas con él. Verá que incluso las aplicaciones con más de mil millones de descargas tienen áreas rojas, así que tómelo con calma cuando intente optimizar.

3. Representación GPU

Esta es otra herramienta de las opciones de desarrollador, llamada representación de GPU de perfil. Al seleccionarlo, elija "En pantalla como barras". Notará que aparecen algunas barras de colores en su pantalla. Dado que cada aplicación tiene barras separadas, curiosamente la barra de estado tiene sus propias barras y, en caso de que tenga botones de navegación de software, también tienen sus propias barras. De todos modos, las barras se actualizan a medida que interactúas con la pantalla.

Representación GPU

Las barras constan de 3 o 4 colores y, según los documentos de Android, su tamaño sí importa. Cuanto más pequeño, mejor. En la parte inferior tiene azul, que representa el tiempo utilizado para crear y actualizar las listas de visualización de la Vista. Si esta parte es demasiado alta, significa que hay mucho dibujo de vista personalizada o mucho trabajo realizado en las funciones onDraw() . Si tiene Android 4.0+, verá una barra morada encima de la azul. Esto representa el tiempo dedicado a transferir recursos al subproceso de procesamiento. Luego viene la parte roja, que representa el tiempo empleado por el renderizador 2D de Android emitiendo comandos a OpenGL para dibujar y redibujar listas de visualización. En la parte superior está la barra naranja, que representa el tiempo que la CPU está esperando a que la GPU termine su trabajo. Si es demasiado alto, la aplicación está haciendo demasiado trabajo en la GPU.

Si eres lo suficientemente bueno, hay un color más por encima del naranja. Es una línea verde que representa el umbral de 16 ms. Como su objetivo debe ser ejecutar su aplicación a 60 fps, tiene 16 ms para dibujar cada cuadro. Si no lo hace, es posible que se salten algunos fotogramas, la aplicación podría volverse irregular y el usuario definitivamente lo notaría. Presta especial atención a las animaciones y al desplazamiento, ahí es donde más importa la fluidez. Aunque puede detectar algunos fotogramas omitidos con esta herramienta, en realidad no le ayudará a averiguar dónde está exactamente el problema.

4. Visor de jerarquía

Esta es una de mis herramientas favoritas, ya que es realmente poderosa. Puede iniciarlo desde Android Studio a través de Tools -> Android -> Android Device Monitor, o también está en su carpeta sdk/tools como "monitor". También puede encontrar un ejecutable hierarachyviewer independiente allí, pero como está obsoleto, debe abrir el monitor. Independientemente de cómo abra Android Device Monitor, cambie a la perspectiva del visor de jerarquía. Si no ve ninguna aplicación en ejecución asignada a su dispositivo, hay un par de cosas que puede hacer para solucionarlo. También intente revisar este hilo de problemas, hay personas con todo tipo de problemas y todo tipo de soluciones. Algo debería funcionar para ti también.

Con Hierarchy Viewer, puede obtener una visión general muy ordenada de sus jerarquías de vista (obviamente). Si ve cada diseño en un XML separado, es posible que detecte fácilmente vistas inútiles. Sin embargo, si continúa combinando los diseños, puede volverse confuso fácilmente. Una herramienta como esta hace que sea sencillo detectar, por ejemplo, algún RelativeLayout, que tiene solo 1 elemento secundario, otro RelativeLayout. Eso hace que uno de ellos sea extraíble.

Evite llamar a requestLayout() , ya que hace que se atraviese toda la jerarquía de vistas para averiguar qué tan grande debe ser cada vista. Si hay algún conflicto con las medidas, la jerarquía puede atravesarse varias veces, lo que si sucede durante alguna animación, definitivamente hará que se salten algunos cuadros. Si desea obtener más información sobre cómo Android dibuja sus vistas, puede leer esto. Veamos una vista como se ve en Hierarchy Viewer.

Visor de jerarquía

La esquina superior derecha contiene un botón para maximizar la vista previa de la vista en particular en una ventana independiente. Debajo también puede ver la vista previa real de la vista en la aplicación. El siguiente elemento es un número, que representa cuántos elementos secundarios tiene la vista dada, incluida la vista en sí. Si selecciona un nodo (preferiblemente el raíz) y presiona "Obtener tiempos de diseño" (3 círculos de colores), tendrá 3 valores más llenos, junto con círculos de colores que aparecerán etiquetados como medida, diseño y dibujo. Puede que no sea sorprendente que la fase de medición represente el tiempo que tomó medir la vista dada. La fase de diseño se trata del tiempo de renderizado, mientras que el dibujo es la operación de dibujo real. Estos valores y colores son relativos entre sí. El verde significa que la vista se representa en el 50% superior de todas las vistas en el árbol. Amarillo significa renderizar en el 50% más lento de todas las vistas en el árbol, rojo significa que la vista dada es una de las más lentas. Como estos valores son relativos, siempre habrá rojos. Simplemente no puedes evitarlos.

Debajo de los valores, tiene el nombre de la clase, como "TextView", una ID de vista interna del objeto y el Android: id de la vista, que establece en los archivos XML. Lo insto a que desarrolle el hábito de agregar ID a todas las vistas, incluso si no hace referencia a ellas en el código. Hará que identificar las vistas en Hierarchy Viewer sea realmente simple y, en caso de que tenga pruebas automatizadas en su proyecto, también hará que la orientación de los elementos sea mucho más rápida. Eso ahorrará algo de tiempo para que usted y sus colegas los escriban. Agregar ID a elementos agregados en archivos XML es bastante sencillo. Pero, ¿qué pasa con los elementos agregados dinámicamente? Bueno, resulta ser muy simple también. Simplemente cree un archivo ids.xml dentro de su carpeta de valores y escriba los campos requeridos. Puede verse así:

 <resources> <item name="item_title" type="id"/> <item name="item_body" type="id"/> </resources>

Luego, en el código, puede usar setId(R.id.item_title) . No podría ser más sencillo.

Hay un par de cosas más a las que prestar atención al optimizar la interfaz de usuario. En general, debe evitar las jerarquías profundas y preferir las poco profundas, tal vez amplias. No utilice diseños que no necesite. Por ejemplo, probablemente pueda reemplazar un grupo de LinearLayouts anidados con RelativeLayout o TableLayout . Siéntase libre de experimentar con diferentes diseños, no use siempre LinearLayout y RelativeLayout . También intente crear algunas vistas personalizadas cuando sea necesario, puede mejorar significativamente el rendimiento si se hace bien. Por ejemplo, ¿sabía que Instagram no usa TextViews para mostrar comentarios?

Puede encontrar más información sobre Hierarchy Viewer en el sitio de desarrolladores de Android con descripciones de diferentes paneles, usando la herramienta Pixel Perfect, etc. Una cosa más que señalaría es capturar las vistas en un archivo .psd, que se puede hacer el botón "Capturar las capas de la ventana". Cada vista estará en una capa separada, por lo que es muy sencillo ocultarla o cambiarla en Photoshop o GIMP. Oh, esa es otra razón para agregar una ID a cada vista que puedas. Hará que las capas tengan nombres que realmente tengan sentido.

Encontrará muchas más herramientas de depuración en las opciones de desarrollador, por lo que le aconsejo que las active y vea qué están haciendo. ¿Qué podría salir mal?

El sitio para desarrolladores de Android contiene un conjunto de prácticas recomendadas para el rendimiento. Cubren muchas áreas diferentes, incluida la gestión de la memoria, de la que realmente no he hablado. Lo ignoré en silencio, porque manejar la memoria y rastrear las fugas de memoria es una historia completamente diferente. El uso de una biblioteca de terceros para mostrar imágenes de manera eficiente ayudará mucho, pero si aún tiene problemas de memoria, consulte Leak canary hecho por Square o lea esto.

Terminando

Entonces, esta era la buena noticia. La mala noticia es que optimizar las aplicaciones de Android es mucho más complicado. Hay muchas formas de hacer todo, por lo que debe estar familiarizado con los pros y los contras de ellas. Por lo general, no existe una solución milagrosa que solo tenga beneficios. Solo al comprender lo que sucede detrás de escena podrá elegir la solución que sea mejor para usted. El hecho de que su desarrollador favorito diga que algo es bueno, no significa necesariamente que sea la mejor solución para usted. Hay muchas más áreas para discutir y más herramientas de creación de perfiles que son más avanzadas, por lo que podríamos abordarlas la próxima vez.

Asegúrese de aprender de los mejores desarrolladores y las mejores empresas. Puede encontrar un par de cientos de blogs de ingeniería en este enlace. Obviamente, no se trata solo de cosas relacionadas con Android, por lo que si solo está interesado en Android, debe filtrar el blog en particular. Recomiendo encarecidamente los blogs de Facebook e Instagram. Aunque la interfaz de usuario de Instagram en Android es cuestionable, su blog de ingeniería tiene algunos artículos realmente interesantes. Para mí es increíble que sea tan fácil ver cómo se hacen las cosas en empresas que manejan cientos de millones de usuarios diariamente, por lo que no leer sus blogs parece una locura. El mundo está cambiando muy rápido, por lo que si no intenta mejorar constantemente, aprender de los demás y utilizar nuevas herramientas, se quedará atrás. Como dijo Mark Twain, una persona que no lee no tiene ventaja sobre una que no sabe leer.