L'utilisation de la mémoire est un aspect du développement sur lequel vous devez vraiment faire attention, sinon cela pourrait ralentir votre application, prendre beaucoup de mémoire, voire tout planter. Ce tutoriel vous aidera à éviter ces mauvais résultats potentiels!
Jetons un coup d'œil au résultat final sur lequel nous allons travailler:
Cliquez n'importe où sur la scène pour créer un effet de feu d'artifice et gardez un œil sur le profileur de mémoire situé dans le coin supérieur gauche..
Si vous avez déjà profilé votre application à l'aide d'un outil de profilage ou utilisé un code ou une bibliothèque qui vous indique l'utilisation actuelle de la mémoire de votre application, vous avez peut-être remarqué que l'utilisation de la mémoire augmente souvent, puis diminue à nouveau. t, votre code est superbe!). Eh bien, bien que ces pics causés par une utilisation importante de la mémoire aient l'air plutôt cool, ce n'est pas une bonne nouvelle pour votre application ni (par conséquent) pour vos utilisateurs. Continuez votre lecture pour comprendre pourquoi cela se produit et comment l'éviter.
L'image ci-dessous est un très bon exemple de mauvaise gestion de la mémoire. Cela provient d'un prototype de jeu. Vous devez remarquer deux choses importantes: les pics importants d'utilisation de la mémoire et le pic d'utilisation de la mémoire. Le pic est presque à 540Mb! Cela signifie que ce prototype à lui seul a atteint le point d'utilisation de 540 Mo de mémoire vive de l'ordinateur de l'utilisateur - ce que vous voulez absolument éviter.
Ce problème commence lorsque vous créez de nombreuses instances d'objet dans votre application. Les instances inutilisées continueront à utiliser la mémoire de votre application jusqu'à ce que le ramasse-miettes s'exécute et se libère, ce qui provoque des pointes importantes. Une situation encore pire se produit lorsque les instances ne sont tout simplement pas désallouées, ce qui entraîne une utilisation croissante de la mémoire de votre application jusqu'à ce que quelque chose se bloque ou tombe en panne. Si vous souhaitez en savoir plus sur ce dernier problème et sur la manière de l’éviter, lisez ce petit conseil relatif à la récupération de place..
Dans ce tutoriel, nous ne traiterons aucun problème de ramasse-miettes. Nous travaillerons plutôt sur la construction de structures qui conservent efficacement les objets dans la mémoire, ce qui rend son utilisation parfaitement stable et empêche ainsi le récupérateur de mémoire de nettoyer la mémoire, ce qui rend l'application plus rapide. Jetez un coup d’œil à l’utilisation de la mémoire du même prototype ci-dessus, mais optimisée cette fois avec les techniques présentées ici:
Toutes ces améliorations peuvent être obtenues à l'aide de la mise en commun d'objets. Lisez la suite pour comprendre ce que c'est et comment ça marche.
Le regroupement d'objets est une technique dans laquelle un nombre prédéfini d'objets est créé lors de l'initialisation de l'application et conservé en mémoire pendant toute la durée de vie de l'application. Le pool d'objets fournit des objets lorsque l'application les demande et réinitialise les objets à l'état initial une fois l'application terminée. Il existe de nombreux types de pools d'objets, mais nous n'en examinerons que deux: les pools d'objets statiques et dynamiques..
Le pool d'objets statiques crée un nombre défini d'objets et ne conserve que cette quantité d'objets pendant toute la durée de vie de l'application. Si un objet est demandé mais que le pool a déjà fourni tous ses objets, le pool renvoie la valeur null. Lors de l'utilisation de ce type de pool, il est nécessaire de résoudre des problèmes tels que la demande d'un objet et l'absence de retour..
Le pool d'objets dynamiques crée également un nombre défini d'objets lors de l'initialisation, mais lorsqu'un objet est demandé et que le pool est vide, le pool crée automatiquement une autre instance et renvoie cet objet, en augmentant la taille du pool et en y ajoutant le nouvel objet..
Dans ce tutoriel, nous allons construire une application simple qui génère des particules lorsque l'utilisateur clique sur l'écran. Ces particules auront une durée de vie finie, puis seront retirées de l'écran et renvoyées dans la piscine. Pour ce faire, nous allons d’abord créer cette application sans regroupement d’objets et vérifier l’utilisation de la mémoire, puis mettre en œuvre le pool d’objets et comparer l’utilisation de la mémoire à celle d’avant..
Ouvrez FlashDevelop (voir ce guide) et créez un nouveau projet AS3. Nous allons utiliser un simple petit carré coloré comme image de particule, qui sera dessiné avec un code et se déplacera selon un angle aléatoire. Créez une nouvelle classe appelée Particle qui étend Sprite. Je suppose que vous pouvez gérer la création d'une particule et mettre en évidence les aspects qui garderont une trace de la durée de vie de la particule et de son retrait de l'écran. Vous pouvez récupérer le code source complet de ce didacticiel en haut de la page si vous rencontrez des problèmes pour créer la particule..
private var _lifeTime: int; mise à jour de la fonction publique (timePassed: uint): void // Déplacement de la particule x + = Math.cos (_angle) * _speed * timePassed / 1000; y + = Math.sin (_angle) * _speed * timePassed / 1000; // Petit soulagement pour rendre le mouvement plus joli _speed - = 120 * timePassed / 1000; // Prendre soin de la vie et de la suppression _lifeTime - = timePassed; si (_lifeTime <= 0) parent.removeChild(this);
Le code ci-dessus est le code responsable de la suppression de la particule de l'écran. Nous créons une variable appelée _durée de vie
contenir le nombre de millisecondes que la particule sera à l'écran. Nous initialisons par défaut sa valeur à 1000 sur le constructeur. le mettre à jour()
La fonction est appelée chaque image et reçoit le nombre de millisecondes écoulées entre les images, afin de réduire la durée de vie de la particule. Lorsque cette valeur atteint 0 ou moins, la particule demande automatiquement à son parent de la supprimer de l'écran. Le reste du code s'occupe du mouvement de la particule.
Nous allons maintenant en créer un groupe lorsqu'un clic de souris est détecté. Allez à Main.as:
private var _oldTime: uint; private var _elapsed: uint; fonction privée init (e: Event = null): void removeEventListener (Event.ADDED_TO_STAGE, init); // point d'entrée stage.addEventListener (MouseEvent.CLICK, createParticles); addEventListener (Event.ENTER_FRAME, updateParticles); _oldTime = getTimer (); fonction privée updateParticles (e: Event): void _elapsed = getTimer () - _oldTime; _oldTime + = _elapsed; pour (var i: int = 0; i < numChildren; i++) if (getChildAt(i) is Particle) Particle(getChildAt(i)).update(_elapsed); private function createParticles(e:MouseEvent):void for (var i:int = 0; i < 10; i++) addChild(new Particle(stage.mouseX, stage.mouseY));
Le code de mise à jour des particules devrait vous être familier: il s’agit des racines d’une simple boucle temporelle, couramment utilisée dans les jeux. N'oubliez pas les déclarations d'importation:
import flash.events.Event; import flash.events.MouseEvent; import flash.utils.getTimer;
Vous pouvez maintenant tester votre application et la profiler à l'aide du profileur intégré de FlashDevelop. Cliquez plusieurs fois à l'écran. Voici à quoi ressemblait mon utilisation de la mémoire:
J'ai cliqué jusqu'à ce que le ramasse-miettes commence à fonctionner. L'application a créé plus de 2000 particules collectées. Cela commence-t-il à ressembler à l'utilisation de la mémoire de ce prototype? Ça y ressemble, et ce n'est vraiment pas bon. Pour faciliter le profilage, nous allons ajouter l'utilitaire mentionné dans la première étape. Voici le code à ajouter dans Main.as:
fonction privée init (e: Event = null): void removeEventListener (Event.ADDED_TO_STAGE, init); // point d'entrée stage.addEventListener (MouseEvent.CLICK, createParticles); addEventListener (Event.ENTER_FRAME, updateParticles); addChild (nouvelles statistiques ()); _oldTime = getTimer ();
N'oubliez pas d'importer net.hires.debug.Stats
et il est prêt à être utilisé!
L'application que nous avons créée à l'étape 4 était assez simple. Il ne comportait qu'un simple effet de particules, mais créait beaucoup de problèmes en mémoire. Dans cette étape, nous allons commencer à travailler sur un pool d'objets afin de résoudre ce problème..
Notre première étape vers une bonne solution consiste à réfléchir à la manière dont les objets peuvent être regroupés sans problèmes. Dans un pool d'objets, nous devons toujours nous assurer que l'objet créé est prêt à être utilisé et que l'objet renvoyé est complètement "isolé" du reste de l'application (c'est-à-dire qu'il ne contient aucune référence à d'autres éléments). Pour forcer chaque objet mis en pool à pouvoir le faire, nous allons créer un interface. Cette interface définira deux fonctions importantes que l’objet doit avoir: renouveler()
et détruire()
. De cette façon, nous pouvons toujours appeler ces méthodes sans nous soucier de savoir si l'objet en a ou non (car il en aura). Cela signifie également que chaque objet que nous voulons regrouper devra implémenter cette interface. Alors la voici:
package interface publique IPoolable fonction est détruite (): Boolean; function renew (): void; fonction destroy (): void;
Puisque nos particules seront mutualisables, nous devons les faire appliquer IPoolable
. Fondamentalement, nous déplaçons tout le code de leurs constructeurs vers le renouveler()
fonction et élimine toute référence externe à l’objet dans la détruire()
une fonction. Voici à quoi cela devrait ressembler:
/ * INTERFACE IPoolable * / la fonction publique est détruite (): Boolean return _destroyed; fonction publique renew (): void if (! _destroyed) return; _destroyed = false; graphics.beginFill (uint (Math.random () * 0xFFFFFF), 0,5 + (Math.random () * 0,5)); graphics.drawRect (-1,5, -1,5, 3, 3); graphics.endFill (); _angle = Math.random () * Math.PI * 2; _ vitesse = 150; // Pixels par seconde _lifeTime = 1000; // Miliseconds fonction publique destroy (): void if (_destroyed) return; _destroyed = true; graphics.clear ();
De plus, le constructeur ne devrait plus avoir besoin d'arguments. Si vous souhaitez transmettre des informations à l'objet, vous devez le faire via des fonctions maintenant. En raison de la façon dont le renouveler()
la fonction fonctionne maintenant, nous devons également définir _détruit
à vrai
dans le constructeur pour que la fonction puisse être exécutée.
Sur ce, nous venons d’adapter notre Particule
classe se comporter comme un IPoolable
. De cette façon, le pool d'objets pourra créer un pool de particules.
Il est temps maintenant de créer un pool d'objets flexible pouvant regrouper tous les objets souhaités. Ce pool agira un peu comme une usine: au lieu d’utiliser le Nouveau
mot-clé pour créer des objets que vous pouvez utiliser, nous appellerons à la place une méthode du pool qui nous renvoie un objet.
Par souci de simplicité, le pool d'objets sera un Singleton. De cette façon, nous pouvons y accéder n'importe où dans notre code. Commencez par créer une nouvelle classe appelée "ObjectPool" et en ajoutant le code pour en faire un Singleton:
package public class ObjectPool privé statique var _instance: ObjectPool; private static var _allowInstantiation: Boolean; fonction statique publique get instance (): ObjectPool if (! _instance) _allowInstantiation = true; _instance = new ObjectPool (); _allowInstantiation = false; return _instance; fonction publique ObjectPool () if (! _allowInstantiation) lance new Error ("Essayer d'instancier un Singleton!");
La variable _allowInstantiation
est le noyau de cette implémentation Singleton: c'est privé, donc seule la propre classe peut modifier, et le seul endroit où elle devrait être modifiée est avant d'en créer la première instance.
Nous devons maintenant décider comment tenir les piscines dans cette classe. Comme il sera global (c’est-à-dire qu'il peut regrouper n’importe quel objet de votre application), nous devons d’abord trouver un moyen de toujours avoir un nom unique pour chaque pool. Comment faire ça? Il existe de nombreuses façons, mais la meilleure solution que j'ai trouvée jusqu'à présent consiste à utiliser les noms de classe de ces objets comme nom de pool. De cette façon, nous pourrions avoir un pool "Particle", un pool "Enemy", etc., mais il y a un autre problème. Les noms de classe doivent uniquement être uniques dans leurs packages. Ainsi, par exemple, une classe "BaseObject" dans le package "ennemis" et une classe "BaseObject" dans le package "structures" sont autorisées. Cela causerait des problèmes dans la piscine.
L’idée d’utiliser des noms de classe comme identifiants pour les pools est toujours bonne, et c’est là que flash.utils.getQualifiedClassName ()
vient nous aider. Fondamentalement, cette fonction génère une chaîne avec le nom complet de la classe, y compris les packages. Alors maintenant, nous pouvons utiliser le nom de classe qualifié de chaque objet comme identifiant pour leurs pools respectifs! C'est ce que nous allons ajouter dans la prochaine étape.
Maintenant que nous avons un moyen d'identifier les pools, il est temps d'ajouter le code qui les crée. Notre pool d'objets doit être suffisamment flexible pour prendre en charge les pools statiques et dynamiques (nous en avons parlé à l'étape 3, vous vous en souvenez?). Nous devons également pouvoir stocker la taille de chaque pool et le nombre d'objets actifs dans chaque pool. Une bonne solution pour cela est de créer une classe privée avec toutes ces informations et de stocker tous les pools au sein d’une Objet
:
package public class ObjectPool privé statique var _instance: ObjectPool; private static var _allowInstantiation: Boolean; private var _pools: Object; fonction statique publique get instance (): ObjectPool if (! _instance) _allowInstantiation = true; _instance = new ObjectPool (); _allowInstantiation = false; return _instance; fonction publique ObjectPool () if (! _allowInstantiation) lance new Error ("Essayer d'instancier un Singleton!"); _pools = ; class PoolInfo éléments var publics: vecteur.; public var itemClass: Class; public var size: uint; public var active: uint; public var isDynamic: Boolean; fonction publique PoolInfo (itemClass: Class, taille: uint, isDynamic: Boolean = true) this.itemClass = itemClass; items = nouveau vecteur. (taille,! isDynamic); this.size = taille; this.isDynamic = isDynamic; actif = 0; initialiser(); fonction privée initialize (): void for (var i: int = 0; i < size; i++) items[i] = new itemClass();
Le code ci-dessus crée la classe privée qui contiendra toutes les informations sur un pool. Nous avons également créé le _ piscines
object pour contenir tous les pools d'objets. Ci-dessous, nous allons créer la fonction qui enregistre un pool dans la classe:
fonction publique registerPool (objectClass: Class, taille: uint = 1, isDynamic: Boolean = true): void if (! (describeType (objectClass). .factory.implementsInterface. (@ type == "IPoolable"). length ()> 0)) jeter une nouvelle erreur ("Impossible de mettre en commun quelque chose qui ne met pas en œuvre IPoolable!"); revenir; nomVarié qualifié: String = getQualifiedClassName (objectClass); if (! _pools [nom qualifié]) _pools [nom qualifié] = new PoolInfo (objectClass, size, isDynamic);
Ce code semble un peu plus compliqué, mais ne paniquez pas. Tout est expliqué ici. La première si
déclaration semble vraiment bizarre. Vous avez peut-être jamais vu ces fonctions auparavant, alors voici ce qu'il fait:
usine
étiquette.implementsInterface
étiquette.IPoolable
l'interface est parmi eux. Si tel est le cas, nous savons que nous pouvons ajouter cette classe au pool, car nous pourrons la convertir avec succès en tant que Je proteste
.Le code après cette vérification crée simplement une entrée dans _ piscines
si on n'existait pas déjà. Après cela, le PoolInfo
constructeur appelle le initialiser()
fonctionner dans cette classe, en créant efficacement le pool avec la taille que nous voulons. Il est maintenant prêt à être utilisé!
Lors de la dernière étape, nous avons pu créer la fonction qui enregistre un pool d’objets, mais nous devons maintenant obtenir un objet pour pouvoir l’utiliser. C'est très simple: nous obtenons un objet si le pool n'est pas vide et le renvoyons. Si le pool est vide, nous vérifions s'il est dynamique. si c'est le cas, nous augmentons sa taille, puis nous créons un nouvel objet et le renvoyons. Sinon, nous retournons null. (Vous pouvez également choisir de générer une erreur, mais il est préférable de simplement renvoyer null et de faire en sorte que votre code contourne cette situation lorsqu'elle se produit.)
Ici se trouve le getObj ()
une fonction:
fonction publique getObj (objectClass: Class): IPoolable var qualifiéName: String = getQualifiedClassName (objectClass); if (! _pools [nom qualifié]) lance une nouvelle erreur ("Impossible d'obtenir un objet d'un pool qui n'a pas été enregistré!"); revenir; var returnObj: IPoolable; if (PoolInfo (_pools [nom qualifié]). active == PoolInfo (_pools [nom qualifié]). size) if (PoolInfo (_pools [nom qualifié]). isDynamic) returnObj = new objectClass (); PoolInfo (_pools [QualifiedName]). Size ++; PoolInfo (_pools [QualifiedName]). Items.push (returnObj); else return null; else returnObj = PoolInfo (_pools [nom qualifié]). items [PoolInfo (_pools [nom qualifié]). active]; returnObj.renew (); PoolInfo (_pools [nom qualifié]). Active ++; return returnObj;
Dans la fonction, nous vérifions d'abord que le pool existe réellement. En supposant que cette condition soit remplie, nous vérifions si le pool est vide: s'il l'est mais est dynamique, nous créons un nouvel objet et ajoutons-le dans le pool. Si le pool n'est pas dynamique, nous arrêtons le code et renvoyons simplement null. Si le pool a toujours un objet, l'objet le plus proche du début du pool et appelé renouveler()
dessus. Ceci est important: la raison pour laquelle nous appelons renouveler()
sur un objet qui était déjà dans la piscine est de garantir que cet objet sera donné à un état "utilisable".
Vous vous demandez probablement pourquoi ne pas utiliser ce chèque sympa avec describeType ()
dans cette fonction? Eh bien, la réponse est simple: describeType ()
crée un XML chaque Il est donc très important d'éviter la création d'objets utilisant beaucoup de mémoire et que nous ne pouvons pas contrôler. De plus, il suffit de vérifier si le pool existe réellement: si la classe transmise n’implémente pas IPoolable
, cela signifie que nous ne serions même pas en mesure de créer un pool pour cela. S'il n'y a pas de piscine pour cela, alors nous attrapons certainement cette affaire dans notre si
déclaration au début de la fonction.
Nous pouvons maintenant modifier notre Principale
classe et utilise le pool d'objets! Vérifiez-le:
fonction privée init (e: Event = null): void removeEventListener (Event.ADDED_TO_STAGE, init); // point d'entrée stage.addEventListener (MouseEvent.CLICK, createParticles); addEventListener (Event.ENTER_FRAME, updateParticles); _oldTime = getTimer (); ObjectPool.instance.registerPool (Particle, 200, true); fonction privée createParticles (e: MouseEvent): void var tempParticle: Particle; pour (var i: int = 0; i < 10; i++) tempParticle = ObjectPool.instance.getObj(Particle) as Particle; tempParticle.x = e.stageX; tempParticle.y = e.stageY; addChild(tempParticle);
Hit compiler et profiler l'utilisation de la mémoire! Voici ce que j'ai eu:
C'est plutôt cool, n'est-ce pas?
Nous avons implémenté avec succès un pool d'objets qui nous fournit des objets. C'est incroyable! Mais ce n'est pas encore fini. Nous n'obtenons toujours que des objets, mais nous ne les retournons jamais quand nous n'en avons plus besoin. Temps d'ajouter une fonction pour retourner des objets à l'intérieur ObjectPool.as
:
fonction publique returnObj (obj: IPoolable): void var qualifiéName: String = getQualifiedClassName (obj); if (! _pools [QualifiedName]) jeter une nouvelle erreur ("Impossible de retourner un objet d'un pool qui n'a pas été enregistré!"); revenir; var objIndex: int = PoolInfo (_pools [nom qualifié]). items.indexOf (obj); if (objIndex> = 0) if (! PoolInfo (_pools [nom qualifié]). isdynamic) PoolInfo (_pools [nom qualifié]). items.fixed = false; PoolInfo (_pools [QualifiedName]). Items.splice (objIndex, 1); obj.destroy (); PoolInfo (_pools [qualifiéName]). Items.push (obj); if (! PoolInfo (_pools [nom qualifié]). isdynamic) PoolInfo (_pools [nom qualifié]). items.fixed = true; PoolInfo (_pools [QualifiedName]). Active--;
Passons en revue la fonction: la première chose à faire est de vérifier s’il existe un pool d’objets transmis. Vous êtes habitué à ce code - la seule différence est que nous utilisons maintenant un objet au lieu d'une classe pour obtenir le nom qualifié, mais cela ne change pas le résultat).
Ensuite, nous obtenons l'index de l'élément dans le pool. Si ce n'est pas dans la piscine, nous l'ignorons. Une fois que nous avons vérifié que l’objet se trouve dans le pool, nous devons casser le pool là où se trouve actuellement l’objet et le réinsérer à la fin. Et pourquoi? Étant donné que nous comptons les objets utilisés depuis le début du pool, nous devons réorganiser le pool pour que tous les objets retournés et inutilisés soient à la fin. Et c'est ce que nous faisons dans cette fonction.
Pour les pools d'objets statiques, nous créons un Vecteur
objet qui a une longueur fixe. Pour cette raison, nous ne pouvons pas épissure()
ça et pousser()
objets en arrière. La solution de contournement à cela est de changer la fixé
propriété de ceux Vecteur
s à faux
, supprimez l'objet et ajoutez-le à la fin, puis modifiez la propriété à vrai
. Nous devons également réduire le nombre d'objets actifs. Après cela, nous avons fini de retourner l'objet.
Maintenant que nous avons créé le code pour retourner un objet, nous pouvons faire en sorte que nos particules retournent elles-mêmes dans le pool une fois qu'elles ont atteint la fin de leur vie. À l'intérieur Particle.as
:
mise à jour de la fonction publique (timePassed: uint): void // Déplacement de la particule x + = Math.cos (_angle) * _speed * timePassed / 1000; y + = Math.sin (_angle) * _speed * timePassed / 1000; // Petit soulagement pour rendre le mouvement plus joli _speed - = 120 * timePassed / 1000; // Prendre soin de la vie et de la suppression _lifeTime - = timePassed; si (_lifeTime <= 0) parent.removeChild(this); ObjectPool.instance.returnObj(this);
Notez que nous avons ajouté un appel à ObjectPool.instance.returnObj ()
dedans là. C'est ce qui fait que l'objet retourne lui-même à la piscine. Nous pouvons maintenant tester et profiler notre application:
Et c'est parti! Mémoire stable même lorsque des centaines de clics ont été effectués!
Vous savez maintenant comment créer et utiliser un pool d'objets afin de maintenir l'utilisation de la mémoire de votre application. La classe que nous avons construite peut être utilisée n’importe où et il est très simple d’y adapter votre code: au début de votre application, créez des pools d’objets pour chaque type d’objet que vous souhaitez mettre en pool. Nouveau
mot-clé (signifiant la création d'une instance), remplacez-le par un appel à la fonction qui obtient un objet pour vous. N'oubliez pas d'implémenter les méthodes que l'interface IPoolable
a besoin!
Garder votre utilisation de la mémoire stable est vraiment important. Cela vous évite bien des soucis plus tard dans votre projet lorsque tout commence à s'effondrer avec des instances non recyclées qui répondent toujours aux écouteurs d'événements, des objets remplissant la mémoire que vous avez à utiliser et avec le ramasse-miettes en marche qui ralentit le tout. Une bonne recommandation est de toujours utiliser la mise en commun des objets à partir de maintenant et vous remarquerez que votre vie sera beaucoup plus facile..
Notez également que, même si ce didacticiel visait Flash, les concepts qu'il développe sont globaux: vous pouvez l'utiliser sur les applications AIR, les applications mobiles et partout où cela vous convient. Merci d'avoir lu!