Cómo optimizo el consumo de memoria para aplicaciones ricas en contenido
Hace 9 meses, comencé mi viaje como estudiante en Apple Developer Academy @Binus, y literalmente no sabía mucho ni tenía experiencia en desarrollo de iOS. Pero apenas sobreviví durante el último mes debido a un entorno de aprendizaje muy solidario (y competitivo al mismo tiempo ).
Para resumir, como aprendiz, aprendí muchos conocimientos en el último mes y la lección más arraigada para mí es la mentalidad de "por qué" y el marco de resolución de problemas.
Tenemos innumerables enfoques para resolver un determinado problema como estudiantes de informática, lo generalizamos en dos tipos: primero en profundidad y primero en amplitud. Pero lo óptimo es la combinación de ambos.

He encontrado muchos trabajos de investigación excelentes al respecto, pero este artículo no cubrirá mucho al respecto, si tiene curiosidad al respecto, puede leer uno de ellos aquí .
Recientemente, como equipo, desarrollé (y sigo desarrollando ) una aplicación llamada ¡ Hayo! en Macro Desafío.

Como puede ver, la interfaz de nuestra aplicación es muy llamativa (felicitaciones a nuestro equipo de diseño que estuvo dispuesto a luchar hasta el último minuto ✊).
Desde una perspectiva técnica, la cantidad de contenido gráfico es lineal al consumo de memoria. También me recuerda mi primer error más dulce en la academia al desarrollar Kelana (mi primera aplicación para iOS). Kelana se considera una aplicación rica en contenido debido a sus numerosas imágenes de alta resolución.


El desempeño de Kelana fue pobre; la página de inicio mostraba 6 imágenes 4K y consumía ≈ 350 MB de RAM. Consume mucha RAM debido a que la clase UIImageView usa la imagen descargada sin procesar (4K) para la interfaz de usuario, aunque el usuario solo la ve como una miniatura pequeña . y no lo noté hasta hace unos meses al observar de cerca lo que sucedía en la memoria de asignación del montón usando Instrument .
Ambos (Kelana y Hayo) se asemejan a cosas similares, como vistas de colección horizontales, activos de imágenes remotos pesados y contenido visual muy rico (contenido rico).
Así que antes del Desarrollo de Hayo! comenzó, estoy retrocediendo unos pasos para aprender algo del viaje pasado como aprendiz, y ahora estoy muy emocionado de contarles sobre el proceso de pensamiento y desglosarlo.
*PD Hay tantas cosas que necesito aprender en el desarrollo de iOS, lo que les voy a decir es sobre mi perspectiva de viaje como estudiante junior. si tiene un mejor enfoque o una mejor práctica, hágamelo saber en el comentario a continuación, para que los demás también puedan aprender sobre él.
¡En este caso, voy a comenzar un proyecto simulado que se asemeje a la característica de Kelana y Hayo! una imagen de ultra alta definición desde una URL remota.
#1 Sepa cuál es el problema y por qué sucede
Ser consciente de lo que sucede en su aplicación es lo primero que debe hacer. Si no sabe por qué existe el problema, entonces lo resolverá a ciegas y tal vez termine forzando las respuestas de StackOverflow por fuerza bruta una por una (*este no es un buen enfoque y soy demasiado perezoso para hacerlo).
En este caso, la aplicación debería cargar una imagen desde una URL remota. Dado que UIImageView no tenía la capacidad de cargarse desde una URL, podríamos agregar una extensión a UIImageView
¿Por qué tiene hilos principales y de fondo anidados?
Separo la obtención de imágenes en un subproceso de fondo y las actualizaciones de la interfaz de usuario en el subproceso principal (relacionado con el rendimiento)
Listo, resolvimos nuestro primer problema e implementamos una aplicación que cargaba imágenes desde URL remotas. Pero espera, ¿la aplicación se siente lenta o solo soy yo? ¿Chrom* se está comiendo mi RAM? es k&#*&@vs a&$n? ¿Por qué sería a^*@ $@ek ?
#2 No se sienta abrumado, apurado o inútil por resolver uno pero creando más problemas.
¿Ya te sentiste abrumado? Cálmate Romeo, lo vamos a solucionar poco a poco.


Como puede ver, el usuario no sabía si la aplicación estaba retrasada o no. Entonces, lo siguiente que podríamos hacer es implementar un indicador de carga para una mejor UX.
Woohoo hemos implementado un indicador que hace que la experiencia del usuario sea mejor y que la carga se sienta más corta. El consumo de memoria sigue siendo malo, pero estamos un paso más cerca.
Desde mi experiencia con las aplicaciones de Kelana...
La clase UIImageView usa la imagen descargada sin procesar (4K) para la interfaz de usuario, aunque el usuario solo la ve como una miniatura pequeña
Por lo tanto, el tamaño del contenido no moderado es nuestra principal preocupación, analicemos la causa raíz.
- Cuanto mayor es el tamaño del contenido, mayor es el consumo de memoria, por lo que la complejidad del espacio es lineal.
- Si el uso de la memoria está aumentando, entonces la huella de la memoria se está ensuciando
- Si el uso de la memoria aumenta de manera extraña, la capacidad de respuesta de la aplicación es menor
- La huella de memoria más limpia, la aplicación de menor probabilidad es eliminada por el recolector de basura de iOS

Tenemos dos opciones para abordar estos problemas. Primero, solicitamos una imagen redimensionada por el tamaño que necesitamos. En segundo lugar, cambiamos el tamaño de la imagen en el dispositivo y mantenemos la versión redimensionada.
El primer enfoque suena como un enfoque de back-end y más allá de nuestra experiencia como ingeniero de iOS, por lo que el segundo enfoque es más factible.


#3 No te apresures, escribe todas las soluciones posibles
¿Resolvimos un problema solo para crear muchos otros problemas? ¿estamos en el camino correcto?
Cuando nos enfrentamos a estas situaciones, naturalmente, como humanos, nuestro impulso de resolver el problema mediante el uso de una solución obvia es lineal con la cantidad de problemas.
Tal vez uno de tus amigos tenía un galimatías (o tú mismo) sobre el caché ya que enfrentamos ciertos problemas de memoria y rendimiento, ¿verdad? Así que vamos a desglosarlo

Como sabemos, según los documentos de Apple, NSCache es un objeto de par clave-valor que almacena un objeto genérico en un contenedor. También dijo que podría minimizar la huella de memoria.
Pensemos de nuevo, el caché podría minimizar la huella de memoria, pero si almacenamos una imagen de 4K en el caché, ¿qué sucedería? Primero, resolvería la capacidad de respuesta porque minimiza el cálculo.
En segundo lugar, la memoria caché también consume espacio tanto como el tamaño del contenido y si el tamaño de la memoria caché es más pequeño que el objeto, es completamente inútil. Pregúntese, ¿vale la pena el ahorro de tiempo por la cantidad de espacio de memoria utilizado?

Punto clave: la memoria caché debe usarse si, solo si, los datos se usan con frecuencia y son un recurso informático costoso
Entonces, en este caso, el almacenamiento en caché de imágenes es demasiado temprano y no es la mejor solución. Demos un paso atrás y veamos desde un ángulo diferente.
En el día a día, a menudo nos enfrentamos a ciertos casos en los que debemos reducir el tamaño del archivo. Hay tantas formas de reducir el tamaño del archivo en este contexto, se llama rasterización . cuando comprimimos un documento de imagen, ¿qué sucede realmente detrás del bit? TLDR mira la ilustración a continuación

Me recuerda cuando estaba aprendiendo desarrollo web en cuarto semestre usando NextJS. NextJS utiliza la rasterización para la optimización de elementos de imagen. En términos muy simples, la resolución de la imagen se comprime con precisión tan grande como el marco de <img> en la pantalla, por lo que reduce el uso de memoria.

En Phot*shop y Next.JS, la rasterización está a solo un clic de distancia, pero ¿cómo hacemos esto en Swift? Desafortunadamente, no hay una función integrada en UIKit (por favor agregue esto para la próxima actualización ). Luego, deberíamos sumergirnos en cómo fluye la representación de imágenes en UIKit.
Según WWDC 18 Session 219 Image and Best Practice de Kyle Sudler, así es como funciona la relación entre UIImage y UIImageView.

Sí, el video tiene 4 años, pero nunca es tarde para aprender, ¿verdad?
También dijo que podríamos ahorrar memoria de manera proactiva en la fase de decodificación mediante la reducción de resolución (generalmente es el mismo concepto que la rasterización).

También adjuntan el código para reducir la resolución de las imágenes usando ImageIO y CoreGraphic

Como de costumbre, copiemos y peguemos esos códigos de muestra en nuestro proyecto.
Uh-oh, la aplicación se bloqueó y TLDR el registro de errores dijo que la URL no debería estar en el hilo principal y sugirió ejecutarla con URLSession asíncrona.

#4 No tengas miedo de leer los documentos oficiales
La Documentación para desarrolladores de Apple es la única fuente de información veraz a la que debe acudir primero. No tengas miedo de pasar un segundo hojeándolo. Personalmente a mi también me agobia pero con el tiempo te acostumbras.

De acuerdo con documentos de muestra, esas funciones necesitan CGImageSource, y para crear CGImageSource necesita CFURL. CFURL solo es capaz de hacer referencia a un recurso local. *CMIIW
Trivia: CG significa CoreGraphic y CF significa CoreFoundation
Significa que se debe acceder a la imagen localmente, pero necesitamos una imagen que se almacene de forma remota en la nube. Entonces esos códigos de muestra de WWDC18 podrían hacer lo que deseábamos .
#5 Replique el proceso, no el código sin procesar
Como ingeniero, nos ganamos la vida copiando y pegando código de StackOverflow. Pero, ¿qué pasaría si no entendemos qué, cómo y por qué funciona el código? Un buen código es un código que funciona consistentemente y es fácil de modificar. Para hacerlo, debe comprender cómo funciona cada componente de su aplicación y cómo interactúa con los demás.
Cuando escribe un buen código, debe comprender por qué existe cada palabra clave y cada carácter. Esto mejora su sofisticación técnica y lo convierte en un mejor ingeniero de software. Menos copiar y pegar significa más pensamiento crítico, ¿verdad?
Hasta ahora, como sabemos, los códigos de muestra de WWDC18 no funcionaron para imágenes remotas. Sin embargo, tenemos una sólida comprensión de por qué, qué y cómo debemos proceder. En nuestro caso, es reducir la resolución de la imagen y dibujarla en la pantalla según el tamaño del marco de la vista de la imagen.
A primera vista, lo primero que viene a la mente es iterar para cada región de la imagen (*paso en términos de visión por computadora) de la imagen y calcular el valor de píxel promedio para cada uno y luego mapearlo en la imagen más pequeña. Si tiene dificultades para entender mi galimatías, mire la ilustración a continuación

En pocas palabras, después de experimentar varias veces, encontré la forma más rápida (y la línea de código más corta) para reducir la resolución de una imagen. Afortunadamente, no tenemos que implementar el algoritmo anterior que UIGraphicsImageRenderer cubre automáticamente . Escribo el código a continuación como una extensión de UIImage para mi preferencia personal sobre la separación de preocupaciones.


Entonces, recapitulemos el progreso que hemos logrado

Pero espera… ¿notas algo mal? Al principio, no me di cuenta, pero miré de cerca entre la imagen original y la versión reducida.

La versión reducida, la relación de aspecto de la imagen se ha estirado debido al tamaño del marco del contenedor (UIImageView). y la versión correcta mantiene la relación de aspecto de la imagen original y se recorta con .scaleAspectFill. Para simplificar, debemos conservar la relación de aspecto de la imagen.
#6 El lápiz y el papel son tus mejores amigos
Después de identificar el problema, estábamos a mitad de camino, así que profundicemos en cómo funciona aspectFill.

Después de ver la imagen de arriba, no es tan complicado como parece antes, ¿no?. con un poco de matemáticas de secundaria podrás lograr lo que deseas.
Después de pasar algunas horas esbozando un enfoque geométrico, me sentí atascado en simplificar el cálculo, pero alguien ya lo resolvió en StackOverflow. (mi mal por no comprobar el StackOverflow primero)


así es como se ve cuando lo implementaste como un código.

Envolver
Hemos estado trabajando en muchas cosas, recapitulemos nuestro viaje para optimizar nuestra aplicación

Desglosando el problema, la solución potencial y el impacto; logramos despejar nuestros pensamientos y hacerlos menos abrumadores, ¿no es así?
y al reducir la resolución de la imagen, logramos reducir el consumo de memoria de 750 MB a 37 MB para 8 imágenes y de 100 MB a 35 MB para una sola imagen. ¿No es eso impresionante?
también por observación, tal vez la complejidad del espacio se redujo de O (n) a O (log n). *CMIIW
Nuestra aplicación ahora consume menos memoria, es más receptiva, tiene menos posibilidades de ser eliminada en segundo plano y consume menos energía. Otras aplicaciones que se ejecutan en el mismo dispositivo tendrán más memoria para trabajar y, como resultado, nuestros consumidores estarán más contentos.
Gracias por leer hasta esta línea, espero que hayan disfrutado este pequeño artículo y que hayan aprendido algo de él. Me encantaría escuchar sus opiniones sobre el tema del desarrollo de iOS o la ingeniería de software en general y no dude en corregirme si encuentra algo engañoso o incorrecto, ya que soy nuevo en el desarrollo de iOS.
Hasta la próxima, Fahri.
© 2022 Fahri Novaldi, Hayo! Equipo de desarrollo. Reservados todos los derechos. Las imágenes están disponibles bajo la licencia internacional Creative Commons Attribution 4.0.