Herramientas y rendimiento

Estrategias prácticas para reducir el espacio en memoria de mapas de bits de tu app

Bocinas

Acerca de

Acerca de la charla

Administrar la memoria de los mapas de bits es fundamental para mitigar los riesgos de falta de memoria (OOM) en las apps para Android con muchos elementos visuales. Si bien los desarrolladores saben que deben reducir el tamaño de las imágenes, para lograr la estabilidad de la app a largo plazo, deben dominar otros dos pilares clave: reutilizar y reciclar. En esta sesión, se ofrece una guía práctica para optimizar el rendimiento de la memoria con herramientas modernas, bibliotecas de código abierto como Glide y Coil, y APIs a nivel del sistema.

En esta sesión, aprenderás a diagnosticar regresiones de memoria reales con Android vitals, implementar patrones de almacenamiento en caché avanzado (reutilización) con Glide y Coil, y liberar memoria de forma proactiva (reciclaje) con la devolución de llamada onTrimMemory.

Resumen

  • Google Play presentará umbrales de rendimiento de memoria que se aplicarán en febrero de 2027. Si superas el límite del percentil 90, se verá afectada directamente la evaluación del estado de tu app y se podría reducir su visibilidad en Play Store.
  • Los mapas de bits se expanden según las dimensiones y la profundidad de color cuando se decodifican, lo que los convierte en los mayores consumidores de memoria en la mayoría de las apps. Solo unas pocas imágenes de resolución completa pueden ocupar cientos de megabytes.
  • Reduce el muestreo de las imágenes y decodifica solo las regiones visibles para reducir el consumo de memoria. Solicita dimensiones de imagen que coincidan a través de la CDN o usa APIs como BitmapRegionDecoder en lugar de cargar imágenes de resolución completa de gran tamaño.
  • Varios cargadores de imágenes multiplican el uso de la caché del montón de memoria predeterminado y duplican los mapas de bits almacenados. Estandariza un cargador o establece presupuestos de memoria explícitos en los cargadores existentes.
  • Libera la memoria de imágenes cuando tu app se mueva a segundo plano controlando onTrimMemory. Detén el sondeo y las animaciones en segundo plano en onStop para evitar que aumente el uso de memoria en segundo plano.

Recursos

Gemini Notebook

Temas

  • bitmap-optimization
  • android-memory
  • android-vitals
  • image-caching
  • memory-profiler

Transcripción

Descargar transcripción

Introducción y descripción general de la memoria de mapas de bits

  • Fung Lam

    Sí, falta aproximadamente 1 hora para el almuerzo, así que quédense conmigo.

  • Soy Fung Lam, ingeniero de relaciones con desarrolladores de Android, y trabajo en Londres.

  • Sí, gracias.

  • Antes de empezar, ¿puedo hacer una pregunta?

  • ¿Cuántos de ustedes compraron un teléfono nuevo este año?

  • ¡Vaya!

  • ¡Vaya!

  • Sé honesto, no te culparé si no compras un Pixel.

  • Bueno, el teléfono nuevo es más caro, ¿verdad?

  • Sí, por eso algunos de ustedes no compraron un teléfono nuevo.

  • ¿Por qué es más caro?

  • ¿Sabes?

  • No.

  • RAM

  • RAM, sí, sí.

  • Sí, la RAM es el verdadero desafío para Android y para nosotros, los desarrolladores.

  • Por eso, hoy estoy aquí para contarles cómo podríamos superar el desafío de la memoria, de la RAM.

  • Anteriormente, en Google I/O y Google I/O Connect, tuvimos algunas sesiones sobre la memoria, desde cómo el sistema operativo asigna la memoria, cómo puedes generar perfiles de tu aplicación, cómo puedes optimizar tu código con R8 y, por supuesto, hablé un poco sobre Bitmap en ese momento.

  • Podías ver mis fotos en la captura de pantalla.

  • La optimización de la memoria es uno de los dominios más grandes, que abarca diferentes áreas, como Android Runtime, la recolección de elementos no utilizados, las herramientas de rendimiento, la asignación de memoria de objetos, el compilador, etcétera. En esta sesión de 30 minutos, te daré información sobre cómo optimizar la memoria de mapas de bits.

  • Tal vez te preguntes por qué Bitmap.

  • ¿Por qué deberíamos enfocarnos en la memoria de mapas de bits?

  • Porque una imagen vale más que mil palabras.

  • Eso es cierto, te explicaré por qué.

  • En la pantalla de un teléfono, por ejemplo, un Pixel 7, un Pixel 9 o un Pixel 11 Pro, la resolución de pantalla es de alrededor de 2,500 × 1,200 o 1,400.

  • Por lo tanto, para cada píxel, tenemos 32 bits de profundidad de color.

  • Por lo tanto, para la imagen de la pantalla completa, oh, no tengo suficientes dedos, pero, por ejemplo, ocupará unos 15 MB de memoria, 50 MB.

  • Por ejemplo, una palabra en el sistema operativo es de 64 bits, lo que significa que es de 8 bytes.

  • Mil palabras son solo ocho, 8 MB.

  • Sí, 8K.

  • Por lo tanto, definitivamente una imagen vale más que mil palabras.

  • Sí, esto es importante.

  • Una imagen ya tiene 15 MB.

  • ¿Qué pasa si tu app tiene 10 o 100 imágenes?

  • Bueno, es un número grande.

  • En realidad, Bitmap siempre suele ser el mayor consumidor de memoria de tu aplicación.

  • Cuando la imagen se decodifica en la memoria, como mencioné, se expande según la dimensión y la profundidad de color.

  • No se trata de, por ejemplo, el tamaño del archivo, si está en formato JPEG o PNG.

  • No, se decodificará y usará la memoria.

  • Por lo tanto, solo unas pocas imágenes de resolución completa ocuparán fácilmente cientos de megabytes de la memoria de tu dispositivo.

  • En segundo lugar, cuando la memoria es muy limitada, el sistema operativo tendrá que gastar más energía para controlar el consumo de memoria.

  • Esto hará que la IU se vea lenta.

  • O bien, el SO también puede cerrar tu app para liberar memoria.

  • O tu app también encontrará un error de memoria insuficiente, una excepción de memoria insuficiente.

  • Por suerte, la buena noticia es que esta fase suele ser una gran victoria con poco esfuerzo.

  • La mayoría de los picos de memoria provienen de errores comunes, que se podrían evitar, y podríamos corregirlos sin comprometer la calidad de la imagen.

  • Ahora ya sabes que la memoria de mapas de bits es importante para tu aplicación, no solo para que funcione sin problemas, sino también para la estabilidad y la retención de usuarios.

  • Por eso, hoy veremos cinco pasos sencillos para que tu app siga funcionando sin problemas.

  • Primero, analizaremos la calidad técnica de Google Play, el nuevo requisito de calidad técnica, que incluye el próximo límite de memoria y dónde puedes verificarlo fácilmente.

  • En segundo lugar, compartiré un caso de estudio sobre 500 aplicaciones para Android, cómo usan la memoria, si lo hacen de la manera correcta o si hay algo que podamos aprender de ellas.

  • Luego, te mostraré el principio para optimizar el uso de memoria de los mapas de bits.

  • Por último, compartiremos algunas herramientas que podrían ayudarte a verificar tu propia app.

Estándares de calidad y problemas habituales de Google Play

  • Fung Lam

    Primero, el nuevo requisito de calidad de Google Play.

  • ¿Con qué frecuencia visitas Google Play Console?

  • ¿Todos los meses?

  • ¿Todas las semanas?

  • ¿Nunca?

  • Esas risas significan que sí, ¿verdad?

  • ¿No?

  • No.

  • ¿Visitaste la sección de Android vitals?

  • Sí, sí.

  • Sí, así que definitivamente, sabes algo antes de que haga esto.

  • Por lo tanto, en Google Play, hay nuevos requisitos de calidad sobre el umbral de rendimiento.

  • En Android y Play, queremos que los usuarios usen apps de buena calidad.

  • Por lo tanto, este umbral también es algo que intentamos mejorar para el usuario final.

  • En pocas palabras, esto significa: "Oye, cuando tu app se ejecuta en segundo plano o en una superficie de primer plano observable, el percentil 90 del uso de memoria no debería superar este umbral.

  • De lo contrario, es posible que tu app tenga menos visibilidad en Play Store o algo similar".

  • La aplicación de medidas por incumplimiento comenzará en febrero de 2027 en las apps y los juegos.

  • Si superas este umbral, se verá afectada directamente la evaluación del estado de tu app.

  • Por lo tanto, asegúrate de ir a Google Play Console y verificar cuál es tu percentil 90 hoy.

  • Y realizar la mejora antes de la fecha límite

  • Esta es la animación.

  • "Oye, si vas a Google Play Console, puedes ver, sí, ahí está Android vitals.

  • Hay una parte de la memoria.

  • Y puedes ver el percentil 90.

  • Y si haces clic en él, sí, puedes ver el promedio del consumo de memoria a lo largo del tiempo.

  • También hay diferentes niveles por RAM, factor de forma, densidad de pantalla y versión del SO, que te ayudan a aislar el cuello de botella de memoria en un dispositivo de nivel de entrada específico o en versiones de apps específicas.

  • Ahora, sé que puedes decir: "Oye, gracias por decirme que mi app supera el umbral.

  • Pero, en serio, no sé qué pasa.

  • ¿Nos ayudarías?".

  • Sí, por eso estoy aquí para ayudarte.

  • Intentaré que sea fácil para ti. Por ejemplo, si estás mirando la TV y, de repente, no hay video ni audio, ¿qué harías?

  • Sacas el manual de la TV, vas a las últimas páginas, que suelen ser la guía de solución de problemas, y dice: "Oye, si tu TV no tiene sonido, prueba este botón o aquel botón".

  • Sí, de manera similar, hoy identifiqué algunos problemas comunes.

  • Esperamos que esta sea tu guía para solucionar problemas.

  • Normalmente, cuando leemos el manual de la TV, debemos ir desde la página uno, que suele ser el kit de herramientas de diagnóstico para ayudarte a entender cómo funciona la TV.

  • Pero ahora que sé que son menos interesantes, hoy, permítanme comenzar con la solución de problemas y volveremos más tarde.

  • Y, para ser honesto, así es como leo el manual de la TV.

  • Para descubrir el problema más frecuente de asignación de memoria de mapa de bits en el ecosistema Android, investigué alrededor de 500 apps populares en Android.

  • Ejecuto la app, la inicio, accedo, voy a diferentes páginas, profundizo y vuelvo.

  • Y pruebo la app en diferentes RAM, también en diferentes dispositivos, como un dispositivo Android Go con 2 gigas de memoria, algunos de nivel básico de 4 gigas, algunos de nivel medio de 4, de 6 a 8 gigas, o algunos plegables premium con una pantalla muy grande, muy buena calidad de pantalla y 16 gigas de memoria.

  • Medimos el uso de memoria en este dispositivo en diferentes estados de la app, capturamos el volcado de montón y usamos diferentes herramientas, incluida la habilidad de IA de Perfetto, para analizar la asignación y, así, identificar antipatrones comunes, es decir, los errores comunes que cometemos, a gran escala.

  • Así es como cambió la memoria en los diferentes estados de la actividad.

  • Con este análisis, identificamos más de 15 problemas comunes, que van desde imágenes de gran tamaño hasta el aumento de las cachés en segundo plano.

  • Este es el caso de éxito principal que compartiremos y exploraremos hoy.

Casos de éxito: Cómo cambiar el tamaño y decodificar mapas de bits

  • Fung Lam

    Sí, todo el caso de éxito que estoy compartiendo ahora se trata de la investigación de 500 apps.

  • Como están aquí, creo que a todos nos encanta Android, ¿verdad?

  • A todos nos encanta Android.

  • Supongamos que sí, y que a uno de nosotros también le encanta Android, pero no es un experto en el desarrollo de Android.

  • Por lo tanto, crea una app para Android que permite pedir figuras de Android.

  • Pero hay algunos errores.

  • Hablemos. Veamos, tal vez este sea el primer error.

  • "Hola, en la página del producto, tenemos un diseño de dos columnas que muestra diferentes figuras de Android.

  • En cada columna, la imagen tiene un tamaño de alrededor de 400 x 400 píxeles.

  • Sin embargo, la app, en realidad, descarga una imagen del servidor, sin importar el tamaño de mi pantalla.

  • Se está descargando una imagen de 3,000 × 3,000.

  • Sí, porque, como desarrollador, "Oye, Android es tan hermoso, tan increíble.

  • Quiero mostrarle lo mejor al usuario".

  • Pero, en realidad, no, porque, oye, está limitado por la dimensión de la vista de imagen.

  • Por lo tanto, incluso si descargas la imagen grande, no se mostrará de la mejor manera al usuario.

  • Además, esta imagen grande consumió memoria adicional.

  • Incluso si reducimos el muestreo, el proceso de reducción también usó memoria adicional.

  • ¿Cómo deberíamos ayudar a este desarrollador a corregirlo?

  • Bueno, si conoces las CDN, sabrás que pueden realizar algunas operaciones con imágenes, como recortar, cambiar el tamaño, rotar, aplicar filtros, convertir una imagen cuadrada en una redonda.

  • Por solicitud HTTP, la CDN podría controlar algo.

  • Por lo tanto, incluso si tu servidor del lado del servidor solo aloja una imagen, puedes usar la CDN para enviar una imagen con el tamaño cambiado a tu dispositivo.

  • Entonces, número 1, sí, cuando solicitas la imagen, intenta usar CDN, para que solicites la imagen con el tamaño de ImageView.

  • Pero si no es posible, como cuando a veces puede ser un poco más grande que ImageView, recuerda reducir el muestreo para cambiar el tamaño en el código.

  • Por lo tanto, cuando se carga en la memoria, se carga una imagen más pequeña, no la resolución completa de la imagen.

  • Aquí tienes una muestra de código para Coil.

    // 1. Ask CDN for the exact UI card dimensions (e.g. 540x675)
    val sizedUrl = "$cdnBaseUrl?w=$cardWidthPx&h=$cardHeightPx&fmt=webp"
    
    // 2. Enforce exact downsampling + Hardware Bitmap config
    AsyncImage(
        model = ImageRequest.Builder(LocalContext.current)
            .data(sizedUrl)
            .size(cardWidthPx, cardHeightPx) // Never use Size.ORIGINAL!
            .precision(Precision.EXACT)      // Downsample to exact card size
            .bitmapConfig(Bitmap.Config.HARDWARE)
            .build(),
        contentDescription = product.title
    )
  • Podrías pasar el tamaño como el texto en amarillo.

  • Sí, podrías pasar el tamaño como HTTP en una solicitud HTTP.

  • Y, luego, aquí, en el código destacado, ¿puedes ver el texto destacado? Sí, no uses Size.ORIGINAL.

  • Sí, usa el ancho y el alto, de modo que se pasen como parámetro.

  • Usa la precisión para reducir el muestreo de la imagen al tamaño exacto.

  • Si es posible, usa Bitmap.Config.HARDWARE, que también usa menos memoria, ya que la imagen se almacenará directamente en la VRAM.

  • De manera similar, en esta app, supongamos que se lanza una nueva versión de los coleccionables de Android, por lo que hay una imagen larga que el usuario puede deslizar hacia la izquierda y la derecha para ver un diseño diferente de la nueva figura de Android.

  • Pero también hay otro problema aquí.

  • En el caso de la imagen larga, gran parte de ella está fuera de la pantalla, lo que significa que el usuario no puede verla, pero la imagen se carga en la memoria.

  • ¿Podríamos hacerlo mejor?

  • Sí.

  • Podríamos cortarla en una imagen más pequeña y mostrar solo la imagen que se muestra en la pantalla.

  • Los que estén más allá de la pantalla no se cargarán o se descargarán.

  • Por lo tanto, usará menos memoria.

  • Del mismo modo, en este caso, se ahorró alrededor del 60% de la memoria.

  • Por supuesto, si recortas un rectángulo pequeño, ahorrarás más.

  • Pero, de manera similar, si un usuario quiere proporcionar una función para el usuario, puedes acercar y acercar para ver cada detalle de la figura de Android, un error común es que realmente cargan, de nuevo, una imagen muy grande, por lo que se puede acercar y acercar de nuevo.

  • Pero no, no tienes que hacerlo.

  • Una mejor manera es cuando el usuario acerca la imagen original de baja resolución ya cargada y, cuando el usuario deja de pellizcar, calculas la coordenada y, luego, cargas la resolución completa, solo cargas esa área, un área calculada de la imagen de alta resolución.

  • Por lo tanto, solo se cargará en la memoria el área visible.

  • ¿Cómo se hace?

  • Hay una API llamada BitmapRegionDecoder, que te ayuda a cargar una región específica de una imagen.

  • Y, por supuesto, también hay algunas herramientas y bibliotecas que podrían ayudarte.

  • Podrías buscarlo en GitHub.

  • Algunas apps de cómics o de mapas ya lo usan.

  • Puedes usarlos como referencia.

  • Además, somos humanos y cometemos errores.

  • La IA también comete errores.

  • Por lo tanto, para evitar que cometamos errores, también podríamos agregar un límite cuando cargamos la imagen.

  • Por ejemplo, en este caso, podría decir: "Usemos el tamaño de la ventana o la dimensión de la pantalla como límite y pongámoslo en una función min, el mínimo del ancho de la solicitud o el ancho del límite".

    // Query active window bounds instead of hardcoding 1080 / 1920
    val bounds = WindowMetricsCalculator.getOrCreate()
        .computeCurrentWindowMetrics(activity).bounds
    
    // Clamp target dimensions so Bitmap NEVER exceeds the active window
    val safeWidth  = minOf(requestedWidthPx,  bounds.width())
    val safeHeight = minOf(requestedHeightPx, bounds.height())
    
    val bitmap = Bitmap.createBitmap(safeWidth, safeHeight, config)
  • Por lo tanto, con esta función, cada vez que cargue una imagen, nunca se saldrá del límite.

  • Por lo tanto, no cargará una imagen más grande que la resolución de pantalla.

  • Recuerda que solo debes decodificar la región visible de la imagen para ahorrar memoria.

Administra la memoria, los duplicados y la caché

  • Fung Lam

    Y, de nuevo, decir: "Oye, esta app, en realidad, no es de un solo desarrollador".

  • En realidad, es para varios colaboradores, y es posible que usen diferentes herramientas.

  • Por ejemplo, parte de ella usa, oh, lo siento.

  • Permíteme empezar de nuevo.

  • Lo mismo ocurre con esta app.

  • Hola, en la página del producto, tengo una lista de productos y vi esta imagen, la agregué al carrito de compras.

  • Unos minutos después, voy a pagar y, oye, vuelve a aparecer la misma imagen.

  • Pero ¿es realmente la misma imagen o, en realidad, se trata de dos objetos diferentes en la memoria?

  • Sí, uno de los errores comunes que encontramos es que la app carga muchas imágenes.

  • Sin embargo, también tienen muchas imágenes duplicadas en la memoria.

  • Si usas un cargador de imágenes, sí, de hecho, podrías pasar una clave para realizar el almacenamiento en caché.

  • Por lo tanto, cuando se usa la misma imagen, puedes usar el objeto Bitmap ya asignado en lugar de crear uno nuevo.

  • En Android Studio, puedes usar el Generador de perfiles de memoria para averiguar si tu app tiene un mapa de bits duplicado.

  • Lo abordé en mi sesión de Google I/O, por lo que puedes volver a YouTube para mirar el video.

  • Y si no usas Android Studio, también hay un comando de adb para capturar el volcado de montón y, luego, puedes usar el lenguaje SQL del gráfico de montón de Perfetto para consultar si mi app tiene un mapa de bits duplicado.

  • Este es un ejemplo de código en Coil, cómo agregar una clave para el almacenamiento en caché de mapas de bits.

    // Strip volatile query tokens (?from=list / ?sig=...) for cache key
    val canonicalKey = MemoryCache.Key(productUrl.substringBefore("?"))
    
    // On Checkout Screen: reuse the 200x200 Bitmap already loaded in the List!
    AsyncImage(
        model = ImageRequest.Builder(context)
            .data(productUrl)
            .placeholderMemoryCacheKey(canonicalKey) // Instant zero-decode reuse!
            .memoryCacheKey(canonicalKey)
            .diskCacheKey(productUrl.substringBefore("?"))
            .build(),
        contentDescription = product.title
    )
  • Y ahora, sí.

  • Volvamos al ejemplo anterior.

  • Hola, mi app tiene un desarrollador diferente.

  • Parte de ella usa Views, por lo que se usa Glide como cargador de imágenes.

  • Y algunos de ellos usan Coil porque usan Compose.

  • Entonces, este cargador de imágenes que acabas de mencionar nos ayuda a almacenar en caché, ¿hay algún problema si tengo más de un cargador de imágenes?

  • Definitivamente, algo está mal.

  • Por lo tanto, para el cargador de imágenes, de forma predeterminada, usarán el X% del tamaño de la memoria del montón como el tamaño de caché predeterminado.

  • Por lo tanto, si tienes un cargador de imágenes, se usa el X% de tu memoria.

  • Si tienes dos cargadores de imágenes, se duplicará la memoria.

  • Si tienes tres cargadores de imágenes, será 3 veces mayor.

  • Y si tu cargador de imágenes no comparte el mismo grupo, sucederá lo mismo.

  • Al igual que en la página del producto, cargo una imagen con Glide y, luego, en la página de confirmación de compra, se cargará la misma imagen con Coil.

  • Sin embargo, habrá dos imágenes en la memoria.

  • Eso no es bueno.

  • Sí, para solucionarlo, lo ideal es usar un solo cargador de imágenes o un grupo compartido para el almacenamiento en caché.

  • Pero ¿qué pasa si esto implica mucho trabajo de ingeniería y no puedo hacerlo?

  • Una solución más sencilla sería medir qué porcentaje de la imagen se controlará con este cargador de imágenes, qué porcentaje de la imagen se controlará con otro cargador de imágenes y establecer un peso para los diferentes cargadores de imágenes, como en este código.

    val totalBudgetBytes = computeAdaptiveCacheBytes(context)
    val numberOfLoaders = 2 // e.g., Team A (Glide) + Team B (Coil)
    
    // Approach A (With Stats): Weight by image traffic (70% Team A / 30% Team B)
    val loaderABudget = (totalBudgetBytes * 0.70).toInt()
    val loaderBBudget = (totalBudgetBytes * 0.30).toInt()
    
    // Approach B (Simple Fix without stats): Divide by number of loaders!
    val simpleSplitBudget = totalBudgetBytes / numberOfLoaders
    
    MemoryCache.Builder()
        .maxSizeBytes(simpleSplitBudget)
        .build()
  • Podría establecer loaderABudget en un 70% y loaderBBudget en un 30%, por lo que esa es otra forma.

  • Si realmente no sabes cuántas imágenes manejará cada cargador, de todos modos, es mejor dividir.

  • Si tengo dos cargadores de imágenes, divido por 2; si tengo tres, divido por 3, en lugar de usar el valor predeterminado, porque eso sobrecargará la memoria.

  • Y supongamos que, en la página del producto, esperamos que el usuario se desplace para ver más del producto.

  • Por lo general, almacenamos en caché alrededor de 50 imágenes o 50 MB de imágenes.

  • Pero ¿qué sucede si la app pasa a segundo plano?

  • Si la app está en segundo plano, es posible que el usuario no esté navegando.

  • Si el usuario no está navegando, ¿debería mantener esta cantidad de imágenes en caché en la memoria?

  • No, deberíamos lanzarlo.

  • Sí, debemos respetar la devolución de llamada onTrimMemory.

  • Sí, de hecho, muchas apps actualmente no implementan o no respetan la devolución de llamada onTrimMemory.

  • Para la devolución de llamada onTrimMemory, hay dos indicadores que debemos tener en cuenta.

  • Uno es UI_HIDDEN, que es como si el usuario hubiera presionado el botón de inicio, y no es inmediatamente después de que lo presiona, sino un período después de que lo hace, lo que significa: "Tu app está en un estado en el que no hay una IU visible para el usuario".

  • Y podríamos esperar: "Oye, el usuario no está usando tu app.

  • Debes liberar los recursos para el SO".

  • Por lo tanto, debemos reducir el tamaño de la caché.

  • Y el otro es TRIM_MEMORY_BACKGROUND, que significa que la app ya está en segundo plano durante mucho tiempo y ahora el SO realmente necesita algunos recursos.

  • En esta etapa, debes liberar la mayor cantidad de memoria posible, casi el 100%.

  • De esta manera, también reduces la probabilidad de que el SO cierre tu app.

  • Porque, por ejemplo, "Oye, quieren memoria, se la das y, luego, ese SO la administra".

  • Si no, el SO no tiene otra opción, tiene que hacer algo.

Cómo evitar el aumento de la memoria en segundo plano

  • Fung Lam

    Otro error importante que veo es, por ejemplo, "Oye, acabo de mencionar que tu app, si la app, si pasa a segundo plano, debería usar menos memoria".

  • Sin embargo, vi que las apps usaban más memoria en segundo plano.

  • ¿Por qué?

  • Por eso investigo.

  • Intenté averiguar por qué.

  • Y una de las razones es: "Oye, en su app, tienen alguna animación o algún carrusel.

  • Por ejemplo, descargan una imagen nueva cada 15 segundos y se la muestran al usuario".

  • Sin embargo, el desarrollador no detuvo la animación cuando la app pasa a segundo plano.

  • Por lo tanto, incluso si la app está en segundo plano, el código sigue ejecutándose, sigue obteniendo imágenes nuevas, decodificándolas y usando más memoria.

  • Por lo tanto, esto es algo que la app no debería hacer.

  • Así es.

  • Deberíamos detener toda la animación, el almacenamiento en caché o el trabajo no relacionado cuando la app pasa a segundo plano.

  • Por lo tanto, si conoces el ciclo de vida de la actividad, hay eventos onPause, onStop, asegúrate de implementar onStop, onPause, onDestroyView para detener la animación, sí, para detener las solicitudes de extracción de imágenes o cualquier otra cosa relacionada con el trabajo que no requiera la atención del usuario.

    override fun onStop() {
        super.onStop()
        // 1. Stop animations so they don't decode next frames in background!
        bannerAnimationView.cancelAnimation()
        animatedImageDrawable?.stop()
    
        // 2. Cancel next-image buffering / prefetch queues!
        nextImagesBufferingJob?.cancel()
        Glide.with(this).pauseRequests()
    }
    
    override fun onDestroyView() {
        super.onDestroyView()
        bannerAnimationView.clearAnimation() // Release resources!
    }

El marco de trabajo de las 3R para la salud de la memoria

  • Fung Lam

    Sé que he hablado de muchos casos contigo, pero dije: "Tengo más de 15 casos".

  • Sé que si te cuento todos los casos, es posible que olvides todo.

  • Cuando vuelvas a casa, buscarás en Google: "¿Cómo soluciono mis problemas de memoria?".

  • Por eso, te facilito la tarea de recordarla.

  • Así es.

  • Lo vincularé con un framework de 3R muy conocido para ayudarte a memorizarlos.

  • Creo que todo el mundo debería haber oído hablar de este marco de las 3R.

  • La primera es reducir, es decir, cómo reducir la memoria cuando se carga una imagen.

  • Por supuesto, como mencioné, podríamos reducir el muestreo y obtener la dimensión correcta de la imagen cuando la obtenemos del servidor.

  • Optimizamos la profundidad de color o usamos un mapa de bits de hardware si es posible.

  • Y, si no usas una CDN, intenta usar una.

  • Por lo tanto, cuando obtenemos la imagen del servidor, solo se obtiene la que mejor se adapta al tamaño.

  • Reutilización: Usamos la caché para que se reutilice la carga de la imagen en la memoria y no tengamos imágenes de mapa de bits duplicadas en nuestra memoria.

  • Y, por supuesto, intenta no tener varios cargadores de imágenes en tu código, ya que eso es menos eficiente.

  • Reduce, reutiliza y ¿cuál es la tercera?

  • ¿Sabes?

  • Recicla.

  • ¿Deseas volver a intentarlo?

  • Reciclar, sí, reciclar.

  • Sí, aprendimos a reducir, reutilizar y reciclar en la escuela primaria, aunque no aprendimos a programar en la escuela primaria.

  • Sí, reciclar, sí.

  • Sí, se trata de cómo liberar la memoria para que otra aplicación o el SO puedan usarla.

  • Hay cosas como el reciclaje.

  • Primero, mencioné que hay un evento onTrimMemory que indica que hay dos señales que el SO te enviará cuando tu IU esté oculta o cuando el SO realmente necesite recursos.

  • Sí, intenta controlar esta solicitud.

  • Y, por supuesto, si tienes una animación, un cargador de imágenes o algún otro trabajo que se esté ejecutando en primer plano, asegúrate de detenerlo cuando se produzca un evento onPause, onStop.

  • Por lo tanto, reduce, reutiliza y recicla.

  • Fácil de recordar, ¿verdad?

Kit de herramientas de diagnóstico para desarrolladores y análisis de IA

  • Fung Lam

    Ahora, volveremos a la parte menos interesante del manual de la TV.

  • ¿Cómo usamos diferentes kits de herramientas para detectar y medir este problema de memoria de manera eficaz?

  • La primera es Google Play Console, Android vitals y la API de Reporting.

  • Sí, consulta el percentil 90 en Google Play Console.

  • ¿Dónde estás ahora?

  • También puedes usar la API de Reporting si no visitas la página web.

  • Aún puedes usar la API para obtener el valor.

  • Sí, asegúrate de que el umbral sea de 200 MB y 400 MB.

  • Asegúrate de no superar el umbral.

  • También hay comandos de ADB que te ayudan, por ejemplo, a capturar el volcado de montón o a enviar un evento para simular que tu app ahora está en una etapa de UI_HIDDEN.

    adb shell dumpsys meminfo <pkg>  # watch Native Heap!
    adb shell am send-trim-memory <pkg> UI_HIDDEN
  • Si usas Android Studio, genial.

  • Sin embargo, siempre es un buen amigo para ti.

  • Puedes usar el generador de perfiles de Android Studio para capturar el volcado de montón y tener una IU que te permita ver cuál es tu uso de memoria y qué es una asignación de memoria.

  • Y, por supuesto, hablamos de IA con Gemini en Android Studio o con otras habilidades de Android disponibles.

  • Te ayuda a analizar el volcado de montón y a usar los seguimientos de Perfetto SQL para encontrar el mapa de bits y, normalmente, saber quién es el propietario de la caché.

  • ¿Glide o Coil cargan mi imagen?

  • ¿Hay alguna otra fuga de memoria en mi aplicación?

  • Sé que algunos de ustedes dicen: "En realidad, ya no uso el IDE.

  • Soy ingeniero de instrucciones.

  • Sí, solo envío instrucciones con el agente de IA para que escriba el código".

  • Sí, lo sé.

  • También hago un poco de vibe coding en mi tiempo libre.

  • Por lo tanto, les comparto esto solo como un ejemplo.

  • Sí, esto es solo una parte de la instrucción que uso para mi investigación de 500 apps.

  • Sí, solo una parte.

  • Sabes que la pantalla no es grande.

  • Si pongo más, apenas se vería.

  • Pero te da una idea de cómo podrías usar la IA para automatizar, capturar el uso de memoria, incluido el volcado de montón, y analizar y proporcionarte hallazgos.

  • Y sí, podría decir aquí, en una oración más, "También proporcióname la corrección de código para mi código".

  • Sí, espero que esto pueda reducir tus temores, decir: "Oye, la memoria es un dominio grande.

  • No estudié sistemas operativos ni arquitectura de sistemas informáticos en la carrera de Ciencias de la Computación en la universidad.

  • Pero, en realidad, esto no es tan aterrador ni tan difícil".

  • Sí, esto te facilita el análisis de tu app.

  • Pruébalo.

  • Pero, por supuesto, la IA cometerá errores, por lo que los conocimientos fundamentales sobre cómo se usa la memoria y cómo el SO la asigna siguen siendo importantes.

  • Por eso, asiste a mi charla y mira nuestro video en YouTube.

Conclusiones clave y resumen

  • Fung Lam

    En resumen, tenemos algunas conclusiones.

  • Primero, reduce.

  • Asegúrate de obtener el tamaño correcto de la imagen.

  • Dividir la imagen en imágenes más pequeñas para no cargar todo en la memoria, sí, solo para cargar en la memoria la imagen que es visible para el usuario.

  • Usa hardware bitmapConfig o RGB_565 siempre que sea posible.

  • Reutiliza, así que asegúrate de tener una estrategia de almacenamiento en caché y usa un cargador de imágenes para reducir la cantidad de mapas de bits duplicados en la memoria.

  • Y, por supuesto, no uses varias imágenes de mapa de bits, incluso si intentas reemplazar el tamaño de caché predeterminado de esos cargadores de imágenes.

  • Y no reutilizamos ni reciclamos.

  • Así es.

  • La administración de la memoria de tu app debe adaptarse al estado de la app.

  • Si la IU está oculta, sí, deberías usar menos memoria.

  • Si el SO realmente te pide que liberes memoria, libera tanta como puedas.

  • Y dejar de agotar los recursos, ya sean de mapa de bits, de energía o de procesamiento.

  • Sí, detén el temporizador del banner, detén la animación y detén la agrupación de imágenes.

  • Por último, asegúrate de supervisar.

  • Ve a Google Play Console con regularidad.

  • Asegúrate de que sea una práctica.

  • Sí, esto te ayudará a comprender cuál es el estado actual del percentil 90 del uso de memoria de tu app en toda la población de usuarios.

  • Esto también te ayudará a identificar los problemas con anticipación.

  • Así es.

  • Para generar perfiles de forma local, puedes usar el Generador de perfiles de memoria de Android Studio.

  • También tenemos algunos comandos de ADB.

  • También tenemos la habilidad de Android.

  • Pero asegúrate de que, si quieres ver las estadísticas de los usuarios reales, Google Play Console es el lugar para ti.

  • También está la API de Google Play Reporting para que puedas obtenerla usando la API sin tener que acceder a Play Console.

  • Como siempre, si quieres obtener más información, visita developer.android.com/memory.

  • Reduce, reutiliza y recicla con esto.

  • Podríamos crear un mejor… Espero que juntos podamos crear un mejor ecosistema de Android para superar el desafío que enfrentamos.

  • Gracias y que disfrutes programando.

  • Pronto tendré un horario de atención afuera, así que, si tienes preguntas, no dudes en venir.

  • Y el próximo orador, bueno, tiene algo de tiempo, así que esperen a que suba al escenario y muchas gracias.

Código

Fragmentos de código

Fragmento de Kotlin en el minuto 11:50 de "Casos de éxito: Cómo ajustar el tamaño de los mapas de bits y decodificarlos"
// 1. Ask CDN for the exact UI card dimensions (e.g. 540x675)
val sizedUrl = "$cdnBaseUrl?w=$cardWidthPx&h=$cardHeightPx&fmt=webp"

// 2. Enforce exact downsampling + Hardware Bitmap config
AsyncImage(
    model = ImageRequest.Builder(LocalContext.current)
        .data(sizedUrl)
        .size(cardWidthPx, cardHeightPx) // Never use Size.ORIGINAL!
        .precision(Precision.EXACT)      // Downsample to exact card size
        .bitmapConfig(Bitmap.Config.HARDWARE)
        .build(),
    contentDescription = product.title
)
Fragmento de Kotlin en el minuto 14:45 de "Case studies: Sizing and decoding Bitmaps"
// Query active window bounds instead of hardcoding 1080 / 1920
val bounds = WindowMetricsCalculator.getOrCreate()
    .computeCurrentWindowMetrics(activity).bounds

// Clamp target dimensions so Bitmap NEVER exceeds the active window
val safeWidth  = minOf(requestedWidthPx,  bounds.width())
val safeHeight = minOf(requestedHeightPx, bounds.height())

val bitmap = Bitmap.createBitmap(safeWidth, safeHeight, config)
Fragmento de Kotlin en el minuto 16:51 de "Administra la memoria, los duplicados y la caché"
// Strip volatile query tokens (?from=list / ?sig=...) for cache key
val canonicalKey = MemoryCache.Key(productUrl.substringBefore("?"))

// On Checkout Screen: reuse the 200x200 Bitmap already loaded in the List!
AsyncImage(
    model = ImageRequest.Builder(context)
        .data(productUrl)
        .placeholderMemoryCacheKey(canonicalKey) // Instant zero-decode reuse!
        .memoryCacheKey(canonicalKey)
        .diskCacheKey(productUrl.substringBefore("?"))
        .build(),
    contentDescription = product.title
)
Fragmento de Kotlin en el minuto 18:17 de "Administración de memoria, duplicados y caché"
val totalBudgetBytes = computeAdaptiveCacheBytes(context)
val numberOfLoaders = 2 // e.g., Team A (Glide) + Team B (Coil)

// Approach A (With Stats): Weight by image traffic (70% Team A / 30% Team B)
val loaderABudget = (totalBudgetBytes * 0.70).toInt()
val loaderBBudget = (totalBudgetBytes * 0.30).toInt()

// Approach B (Simple Fix without stats): Divide by number of loaders!
val simpleSplitBudget = totalBudgetBytes / numberOfLoaders

MemoryCache.Builder()
    .maxSizeBytes(simpleSplitBudget)
    .build()
Fragmento de Kotlin en el minuto 22:05 de "Cómo evitar el aumento de la memoria en segundo plano"
override fun onStop() {
    super.onStop()
    // 1. Stop animations so they don't decode next frames in background!
    bannerAnimationView.cancelAnimation()
    animatedImageDrawable?.stop()

    // 2. Cancel next-image buffering / prefetch queues!
    nextImagesBufferingJob?.cancel()
    Glide.with(this).pauseRequests()
}

override fun onDestroyView() {
    super.onDestroyView()
    bannerAnimationView.clearAnimation() // Release resources!
}
Fragmento de shell en el minuto 25:17 de "Kit de herramientas de diagnóstico para desarrolladores y análisis de IA"
adb shell dumpsys meminfo <pkg>  # watch Native Heap!
adb shell am send-trim-memory <pkg> UI_HIDDEN