Comment j'optimise la consommation de mémoire pour les applications riches en contenu
Il y a 9 mois, j'ai commencé mon parcours en tant qu'apprenant à l'Apple Developer Academy @Binus, et je ne connaissais littéralement pas grand-chose et aucune expérience dans le développement iOS. Mais j'ai à peine survécu le mois dernier à cause d'un environnement d'apprentissage très favorable (et compétitif en même temps ).
Pour faire court, en tant qu'apprenant, j'ai appris tellement de connaissances au cours du mois dernier et la leçon la plus enracinée pour moi est la mentalité du "pourquoi" et le cadre de résolution de problèmes.
Nous avons d'innombrables approches pour résoudre un certain problème en tant qu'étudiants en informatique, nous le généralisons en deux types Profondeur d'abord et Largeur d'abord. Mais l'optimum est la combinaison des deux.

J'ai trouvé beaucoup de superbes articles de recherche à ce sujet, mais cet article ne couvrira pas grand-chose à ce sujet, si vous êtes curieux à ce sujet, vous pouvez en lire un ici .
Récemment, en tant qu'équipe, j'ai développé (et développe toujours ) une application appelée Hayo ! dans Macro Challenge.

Comme vous pouvez le constater, l'interface de notre application est très accrocheuse (bravo à notre équipe de conception qui a été prête à lutter jusqu'à la dernière minute ✊).
D'un point de vue technique, le nombre de contenus graphiques est linéaire par rapport à la consommation de mémoire. Cela me rappelle aussi ma première erreur la plus douce à l'académie dans le développement de Kelana (ma première application iOS). Kelana est considérée comme une application riche en contenu en raison de ses nombreuses images haute résolution.


La performance de Kelana était médiocre; la page d'accueil affichait 6 images 4K et consommait ≈ 350 Mo de RAM. Il consomme tellement de RAM car la classe UIImageView utilise l'image brute téléchargée (4K) pour l'interface utilisateur même si l'utilisateur ne la voit que comme une petite vignette . et je ne l'ai remarqué qu'il y a quelques mois en surveillant de près ce qui se passait dans la mémoire d'allocation de tas à l'aide de Instrument .
Les deux (Kelana & Hayo) ressemblent à des choses similaires comme des vues de collection horizontales, de lourds actifs d'images distantes et un contenu visuel très riche (Content-Rich).
Alors avant le développement de Hayo! commencé, je prends quelques pas en arrière pour apprendre quelque chose du parcours passé en tant qu'apprenant, et maintenant je suis très heureux de vous parler du processus de réflexion et de le décomposer.
*PS Il y a tellement de choses que j'ai besoin d'apprendre dans le développement iOS, ce que je vais vous dire concerne ma perspective de parcours en tant qu'apprenant junior. si vous avez une meilleure approche ou une meilleure pratique, faites-le moi savoir dans le commentaire ci-dessous, afin que les autres puissent également en apprendre davantage.
Dans ce cas, je vais commencer un projet fictif qui ressemble à la caractéristique de Kelana et Hayo ! une image ultra haute définition à partir d'une URL distante.
#1 Sachez quel est le problème et pourquoi cela se produit
Être conscient de ce qui se passe dans votre application est la première chose que vous devez faire. Si vous ne savez pas pourquoi le problème existe, alors vous allez les résoudre aveuglément et peut-être finir par forcer brutalement les réponses de StackOverflow une par une (* ce n'est pas une bonne approche et je suis trop paresseux pour faire ça).
Dans ce cas, l'application doit charger une image à partir d'une URL distante. Étant donné que UIImageView n'avait pas la possibilité de charger à partir d'une URL, nous pourrions ajouter une extension à UIImageView
Pourquoi a-t-il un arrière-plan et des fils principaux imbriqués ?
Je sépare la récupération d'image sur un fil d'arrière-plan et les mises à jour de l'interface utilisateur sur le fil principal (lié aux performances)
Voilà, nous avons résolu notre premier problème et implémenté une application qui chargeait l'image à partir d'URL distantes. Mais attendez, est-ce que l'application semble lag ou est-ce juste moi? Est-ce que chrom* ne fait que manger ma RAM ? est k&#*&@vs un&$n ? Pourquoi serait-ce un ^*@ $@ek ?
# 2 Ne vous sentez pas dépassé, pressé ou inutile pour en résoudre un, mais en créant plus de problèmes.
Vous êtes-vous déjà senti dépassé ? Ralentissez Roméo, on va le résoudre petit à petit.


Comme vous pouvez le voir, l'utilisateur ne savait pas si l'application était en retard ou non. Donc, la prochaine chose que nous pourrions faire est d'implémenter un indicateur de chargement pour une meilleure UX.
Woohoo nous avons mis en place un indicateur qui rend l'expérience utilisateur meilleure et le chargement ressenti plus court. La consommation de mémoire est toujours mauvaise mais nous sommes un peu plus près.
De mon expérience avec les applications Kelana…
La classe UIImageView utilise l'image brute téléchargée (4K) pour l'interface utilisateur même si l'utilisateur ne la voit que comme une petite vignette
La taille du contenu non modéré est donc notre principale préoccupation, décomposons-la à l'aide d'une analyse des causes profondes.
- Plus la taille du contenu est grande, plus la consommation de mémoire est grande, donc la complexité de l'espace est linéaire.
- Si l'utilisation de la mémoire augmente, l'empreinte mémoire devient sale
- Si l'utilisation de la mémoire augmente bizarrement, la réactivité de l'application est moindre
- L'empreinte mémoire plus propre, l'application moins susceptible d'être tuée par le ramasse-miettes iOS

Nous avons deux options pour résoudre ces problèmes. Tout d'abord, nous demandons une image redimensionnée par la taille dont nous avons besoin. Deuxièmement, nous redimensionnons l'image sur l'appareil et conservons la version redimensionnée.
La première approche ressemble à une approche backend et au-delà de notre expertise en tant qu'ingénieur iOS, la deuxième approche est donc plus réalisable.


#3 Ne vous précipitez pas, notez toutes les solutions possibles
Avons-nous résolu un problème pour en créer de nombreux autres ? sommes-nous sur la bonne voie ?
Lorsque nous sommes confrontés à ces situations, naturellement, en tant qu'être humain, notre envie de résoudre le problème en utilisant une solution simple est linéaire par rapport au nombre de problèmes.
Peut-être qu'un de vos amis avait du charabia (ou vous-même) à propos du cache puisque nous rencontrons certains problèmes de mémoire et de performances, n'est-ce pas ? Alors décomposons-le

Comme nous le savons, selon les documents Apple, NSCache est un objet paire clé-valeur qui stocke un objet générique dans un conteneur. Il a également déclaré qu'il pourrait minimiser l'empreinte mémoire.
Détrompons-nous, le cache pourrait minimiser l'empreinte mémoire, mais si nous stockons une image 4K dans le cache, que se passerait-il ?? Premièrement, cela résoudrait la réactivité car cela minimise le calcul.
Deuxièmement, le cache consomme autant d'espace que la taille du contenu et si la taille du cache est plus petite que l'objet, il est complètement inutile. Demandez-vous si le gain de temps vaut la quantité d'espace mémoire utilisée ?

Point clé : le cache doit être utilisé si-seulement-si les données sont fréquemment utilisées et constituent une ressource informatique coûteuse
Donc, dans ce cas, la mise en cache des images est trop tôt et n'est pas la meilleure solution. Prenons du recul et voyons sous un autre angle.
Au quotidien, nous sommes souvent confrontés à certains cas où nous devrions réduire la taille du fichier. Il existe tellement de façons de réduire la taille du fichier dans ce contexte, cela s'appelle la rastérisation . lorsque nous compressons un document image, que se passe-t-il réellement derrière le bit ? TLDR regardez l'illustration ci-dessous

Cela me rappelle quand j'apprenais le développement web en 4ème semestre en utilisant NextJS. NextJS utilise la rastérisation pour l'optimisation des éléments d'image. En termes simplifiés, la résolution de l'image est compressée aussi grande que le cadre de <img> à l'écran, ce qui réduit l'utilisation de la mémoire.

Dans Phot*shop & Next.JS, la pixellisation n'est qu'à un clic, mais comment procédons-nous dans Swift ? Malheureusement, il n'y a pas de fonction intégrée sur UIKit (veuillez l'ajouter pour la prochaine mise à jour ). Ensuite, nous devrions nous plonger dans le flux de rendu d'image chez UIKit.
Selon WWDC 18 Session 219 Image and Best Practice par Kyle Sudler, voici comment la relation entre UIImage et UIImageView.

Oui la vidéo a 4 ans, mais il n'est jamais trop tard pour apprendre, non ?
Il a également dit que nous pouvions économiser de manière proactive de la mémoire dans la phase de décodage en sous-échantillonnant (généralement, c'est le même concept que la rastérisation).

Ils attachent également le code pour sous-échantillonner les images à l'aide d'ImageIO & CoreGraphic

Comme d'habitude, copions-collons ces exemples de codes dans notre projet.
Uh-oh, l'application s'est écrasée et TLDR le journal des erreurs a déclaré que l'URL ne devrait pas être sur le fil principal et a suggéré de l'exécuter avec URLSession asynchrone.

#4 N'ayez pas peur de lire les Docs officiels
La documentation pour les développeurs d'Apple est la seule source de vérité à laquelle vous devez d'abord vous adresser. N'ayez pas peur de passer une seconde à le parcourir. Personnellement, ça me dépasse aussi mais on s'y habitue avec le temps.

Selon des exemples de documentation, ces fonctions ont besoin de CGImageSource et, pour créer CGImageSource, elles ont besoin de CFURL. CFURL est uniquement capable de référencer une ressource locale. *CMIIW
Anecdote : CG signifie CoreGraphic et CF signifie CoreFoundation
Cela signifie que l'image doit être accessible localement, mais nous avons besoin d'une image stockée à distance sur le cloud. Ainsi, ces exemples de codes de WWDC18 pourraient faire ce que nous souhaitions .
#5 Répliquez le processus, pas le code brut
En tant qu'ingénieur, nous copions-collons le code de StackOverflow pour gagner notre vie. Mais que se passerait-il si nous ne comprenions pas quoi, comment et pourquoi le code fonctionne ? Un bon code est un code qui fonctionne de manière cohérente et qui est facile à modifier. Pour ce faire, vous devez comprendre comment chaque composant de votre application fonctionne et comment il interagit avec les autres.
Lorsque vous écrivez du bon code, vous devez comprendre pourquoi chaque mot-clé et caractère existe. Cela améliore votre sophistication technique et fait de vous un meilleur ingénieur logiciel. Moins de copier-coller signifie plus de pensée critique, n'est-ce pas ?
Jusqu'à présent, comme nous le savons, les exemples de codes de WWDC18 ne fonctionnaient pas pour les images distantes. Cependant, nous avons une solide compréhension de pourquoi, quoi et comment nous devrions procéder. Dans notre cas, il s'agit de sous-échantillonner l'image et de la dessiner à l'écran en fonction de la taille du cadre de la vue de l'image.
À première vue, la première chose qui vient à l'esprit est d'itérer pour chaque région de l'image (* foulée en termes de vision par ordinateur) de l'image et de calculer la valeur moyenne des pixels pour chacune, puis de la mapper dans l'image plus petite. Si vous avez du mal à comprendre mon charabia, regardez l'illustration ci-dessous

Bref, après avoir expérimenté plusieurs fois, j'ai trouvé le moyen le plus rapide (et la ligne de code la plus courte) de sous-échantillonner une image. Heureusement, nous n'avons pas à implémenter l'algorithme ci-dessus qui est automatiquement couvert par UIGraphicsImageRenderer . J'écris le code ci-dessous en tant qu'extension de UIImage pour ma préférence personnelle sur la séparation des préoccupations.


Alors récapitulons les progrès que nous avons réalisés

Mais attendez… avez-vous remarqué quelque chose qui ne va pas ? Au début, je n'y avais pas fait attention mais regardez attentivement entre l'image originale et la version sous-échantillonnée

La version sous-échantillonnée, le rapport d'aspect de l'image a été étiré en raison de sa taille de cadre de conteneur (UIImageView). et la bonne version conserve le rapport d'aspect de l'image d'origine et est recadrée par .scaleAspectFill. Pour être simplifié, nous devons conserver le rapport d'aspect de l'image.
#6 Le stylo et le papier sont vos meilleurs amis
Après avoir identifié le problème, nous étions à mi-chemin, alors plongeons dans le fonctionnement de aspectFill.

Après avoir vu l'image ci-dessus Ce n'est pas compliqué comme il y paraît avant, n'est-ce pas ?. avec un peu de mathématiques du secondaire, vous serez en mesure de réaliser ce que vous vouliez.
Après avoir passé quelques heures à esquisser une approche géométrique, je me suis senti bloqué pour simplifier le calcul, mais quelqu'un l'a déjà résolu sur StackOverflow. (mon mauvais pour ne pas vérifier le StackOverflow en premier)


voici à quoi cela ressemble lorsque vous l'avez implémenté en tant que code.

Emballer
Nous avons travaillé tellement de choses, récapitulons notre voyage sur l'optimisation de notre application

En décomposant le problème, la solution potentielle et l'impact ; nous parvenons à désencombrer nos pensées et à les rendre moins accablantes n'est-ce pas ?
et en sous-échantillonnant l'image, nous parvenons à réduire l'empreinte mémoire de 750 Mo à 37 Mo pour 8 images et de 100 Mo à 35 Mo pour une seule image ! N'est-ce pas impressionnant ?
également par observation, peut-être que la complexité spatiale a été réduite de O(n) à O(log n). *CMIIW
Notre application consomme désormais moins de mémoire, est plus réactive, a moins de possibilité d'être tuée en arrière-plan et consomme moins d'énergie. D'autres applications exécutées sur le même appareil auront plus de mémoire pour fonctionner, et nos consommateurs seront plus heureux en conséquence.
Merci d'avoir lu jusqu'à cette ligne, j'espère que vous avez apprécié ce petit article et que vous en avez appris quelque chose. J'aimerais entendre vos réflexions sur le thème du développement iOS ou du génie logiciel en général et n'hésitez pas à me corriger si vous avez trouvé quelque chose de trompeur ou de faux puisque je suis nouveau dans le développement iOS.
Jusqu'à la prochaine fois, Fahri.
© 2022 Fahri Novaldi, Hayo! Équipe de développement. Tous les droits sont réservés. Les images sont disponibles sous la licence internationale Creative Commons Attribution 4.0.