Une perte de mémoire est si vite arrivée
J'ai vraiment peur de l'AS3 parfois. J'ai peur que tout explose d'un coup.
Avant de commencer deux liens en rapport direct avec le sujet :
C'est partie...
2 tubes à essais + un témoin; un Bec Bunsen !
Prenons par exemple la classe de document test suivante :
package { import flash.display.Sprite; import flash.events.Event; public class MemoryLeaksTest extends Sprite { public function MemoryLeaksTest() { new Sprite().addEventListener ( Event.EXIT_FRAME, new SetTimeoutTest().enterFrame, false, 0, true //on utilise les weak référence ); new Sprite().addEventListener ( Event.EXIT_FRAME, new AnonymousFuncTest().enterFrame, false, 0, true //on utilise les weak référence ); new Sprite().addEventListener ( Event.EXIT_FRAME, new NoLeakTest().enterFrame, false, 0, true //on utilise les weak référence ); } } }
et les classes associées qui se ressemblent beaucoup (le tube témoin et les autres tubes à essais) :
NoLeakTest.as
(pas de perte de mémoire)package { import flash.events.Event; import flash.utils.setTimeout; /** * ... * @author Julien BEAUFILS */ public class NoLeakTest { public function NoLeakTest() { } public function enterFrame(e:Event):void { trace(this); } } }
AnonymousFuncTest.as
Déclaration d'une fonction anonyme dans une variable local ou de classe, ou comme propriété d'un objet.
Mauvaise pratique connue, parfois tentante et utilisée en interne de librairies populaires, voir même encouragée par l'exemple.
package { import flash.events.Event; import flash.utils.setTimeout; /** * ... * @author Julien BEAUFILS */ public class AnonymousFuncTest { public function AnonymousFuncTest() {var func:Function = function() { };} public function enterFrame(e:Event):void { trace(this); } } }
SetTimeoutTest.as
Utilisation de setTimeout pour différer légèrement une action / pour séquencer quelque chose...
C'est tellement tentant.
package { import flash.events.Event; import flash.utils.setTimeout; /** * ... * @author Julien BEAUFILS */ public class SetTimeoutTest { public function SetTimeoutTest() { setTimeout(func, 100); } private function func():void {} public function enterFrame(e:Event):void { trace(this); } } }
Résultats de l'expérieance
On verras ces deux lignes se répéter en permanence en sortie :
[object SetTimeoutTest] [object AnonymousFuncTest]
CE N'EST PAS NORMAL
- Les instances ne sont référencées explicitement nulle part dans le code et pourtant elles sont bien là, se nommant en permanence sur la fenêtre de sortie.
- On aura beau faire du clearTimeout d'un identifiant référencé, passer des variables à null etc...
Le seul moyen de stopper cette sortie est de supprimer les écouteurs explicitement.
- Les objets n'ont pas quitté la mémoire. la suppression explicite des écouteur est indispensable mais absoluement pas suffisante.
La solution finale
- Exterminer les fonctions anonyme (bien que les autres les utilisent).
- Supprimer du vocabulaire setTimeout et setInterval bien qu'on les utilise parfois également sans en avoir conscience.
- Croiser les doigts et tester la mémoire.
Fort intéressant et amusant cet article!
On constate que les langages dit de "haut-niveaux" poussés à leurs extrêmes, dans leurs derniers retranchements, comme tu aimes à le faire ^^, mettent en évidence trois choses :
1/ Ils fournissent une abstraction importante du langage machine qui permet de voir plus loin, de créer des nouvelles applications.
2/ Les abstractions si vastes mettent nécessairement à rude épreuve un système déjà instable par nature (électronique + humain), tôt ou tard.
3/ Le garbage collector est une fausse bonne idée ;)
Bonne continuation pour ton blog et tes expériences !
hello, j'ai peut etre pas tout capté, mais tu recrées bien une nouvelle instance de ta classe test à chaque frame, donc normal que tu ais un trace derriere.
Si tu leur colles un nom unique à tes instances, peut etre les verrons t on se faire gicler par le GC ? (pas le temps de tester pour etre sur de moi)
Les trois classes de test ne sont instanciées qu'une fois (un trace dans le constructeur permet de le vérifier)
Référencer temporairement les instances dans des variables (locales ou globales), et utiliser des id pour les appels à setTimeout ne change rien (j'ai pas mal trituré ça avant d'écrire ce billet).
De plus le "tube témoin" (NoLeakTest) permet de mettre en évidence les parties qui pausent problème, car cette classe fonctionne comme attendu. à chaque déclenchement de l'événement écouté on a :
et non :
par contre je vais corriger de ce pas une erreur :
je voulais dire "Les objets n'ont pas quitté la mémoire" et pas "Les objets ont quitté la mémoire", bien qu'ils soient exposés directement au GC.