Houd het gebruik van het geheugen van uw Flash-project stabiel bij het poolen van objecten

Geheugengebruik is een aspect van ontwikkeling waar je echt voorzichtig mee moet zijn, of het kan uiteindelijk je app vertragen, veel geheugen in beslag nemen of zelfs alles laten crashen. Deze tutorial helpt je om die slechte potentiële uitkomsten te vermijden!


Eindresultaat voorbeeld

Laten we eens kijken naar het eindresultaat waar we naartoe zullen werken:

Klik ergens op het podium om een ​​vuurwerkeffect te maken en houd de geheugenprofiler in de linkerbovenhoek in de gaten.


Stap 1: Introductie

Als u ooit uw toepassing hebt geprofileerd met behulp van een profileerhulpprogramma of een code of bibliotheek hebt gebruikt die u het huidige geheugengebruik van uw toepassing vertelt, is het u wellicht opgevallen dat het geheugengebruik vaak omhoog gaat en vervolgens weer daalt (als u niet, je code is geweldig!). Hoewel deze spikes veroorzaakt door een groot geheugengebruik best cool lijken, is het geen goed nieuws voor zowel je applicatie als (bijgevolg) je gebruikers. Blijf lezen om te begrijpen waarom dit gebeurt en hoe je dit kunt vermijden.


Stap 2: Goed en slecht gebruik

De onderstaande afbeelding is een geweldig voorbeeld van slecht geheugenbeheer. Het is van een prototype van een spel. U moet twee belangrijke dingen opmerken: de grote pieken in het geheugengebruik en de piek in geheugengebruik. De piek is bijna 540 MB! Dat betekent dat dit prototype alleen al het punt bereikte om 540 MB RAM-geheugen van de gebruiker te gebruiken - en dat is iets dat je absoluut wilt vermijden.

Dit probleem begint wanneer u begint met het maken van veel objectexemplaren in uw toepassing. Ongebruikte exemplaren blijven het geheugen van uw toepassing gebruiken totdat de vuilnisophaler wordt uitgevoerd, wanneer ze worden verwijderd - waardoor de grote pieken ontstaan. Een nog ergere situatie doet zich voor wanneer de instanties eenvoudig niet worden verwijderd, waardoor het geheugengebruik van uw toepassing blijft groeien totdat er iets vastloopt of breekt. Als u meer wilt weten over het laatste probleem en hoe u dit kunt voorkomen, lees dan deze snelle tip over garbagecollection.

In deze zelfstudie behandelen we geen problemen met garbagecolors. In plaats daarvan werken we aan het bouwen van structuren die efficiënt objecten in het geheugen houden, waardoor het gebruik ervan volledig stabiel is en de vuilnisman dus niet meer het geheugen opruimt, waardoor de toepassing sneller wordt. Bekijk het geheugengebruik van hetzelfde prototype hierboven, maar deze keer geoptimaliseerd met de hier getoonde technieken:

Al deze verbeteringen kunnen worden bereikt met behulp van object pooling. Lees verder om te begrijpen wat het is en hoe het werkt.


Stap 3: soorten pools

Objectgroepering is een techniek waarbij een vooraf gedefinieerd aantal objecten wordt gemaakt wanneer de toepassing wordt geïnitialiseerd en gedurende de gehele levensduur van de toepassing in het geheugen wordt bewaard. De objectgroep geeft objecten wanneer de toepassing ze aanvraagt ​​en stelt de objecten terug naar de oorspronkelijke status wanneer de toepassing klaar is met het gebruik ervan. Er zijn veel soorten objectpools, maar we zullen er slechts twee bekijken: de statische en de dynamische objectpools.

De pool met statische objecten maakt een gedefinieerd aantal objecten en bewaart alleen die hoeveelheid objecten gedurende de hele levensduur van de toepassing. Als een object wordt aangevraagd maar de pool al al zijn objecten heeft gegeven, wordt de pool null geretourneerd. Bij het gebruik van dit soort pool is het nodig om zaken aan te pakken zoals het aanvragen van een object en niets terugkrijgen.

De dynamische objectpool maakt ook een gedefinieerd aantal objecten bij initialisatie, maar wanneer een object wordt aangevraagd en de pool leeg is, maakt de pool automatisch een andere instantie automatisch en retourneert het dat object, waardoor de poolgrootte wordt vergroot en het nieuwe object eraan wordt toegevoegd.

In deze tutorial zullen we een eenvoudige applicatie bouwen die deeltjes genereert wanneer de gebruiker op het scherm klikt. Deze deeltjes hebben een eindige levensduur en worden vervolgens van het scherm verwijderd en teruggestuurd naar het zwembad. Om dit te doen, zullen we eerst deze applicatie maken zonder object pooling en het geheugengebruik controleren en dan de object pool implementeren en het geheugengebruik vergelijken met eerder.


Stap 4: eerste applicatie

Open FlashDevelop (zie deze handleiding) en maak een nieuw AS3-project. We zullen een eenvoudig klein gekleurd vierkant gebruiken als het deeltjesbeeld, dat getekend zal worden met code en zal bewegen volgens een willekeurige hoek. Maak een nieuwe klasse genaamd Particle die Sprite uitbreidt. Ik neem aan dat je de creatie van een deeltje aankan, en alleen de aspecten markeert die de levensduur van het deeltje bijhouden en verwijderen van het scherm. Je kunt de volledige broncode van deze zelfstudie boven aan de pagina bekijken als je problemen ondervindt bij het maken van het deeltje.

 privé var _lifeTime: int; update van publieke functie (timePassed: uint): void // Deelbeweging maken x + = Math.cos (_angle) * _speed * timePassed / 1000; y + = Math.sin (_angle) * _speed * timePassed / 1000; // Kleine versnelling om de beweging er mooi uit te laten zien _snelheid - = 120 * timePassed / 1000; // Verzorgen van de levensduur en verwijderen _lifeTime - = timePassed; if (_lifeTime <= 0)  parent.removeChild(this);  

De bovenstaande code is de code die verantwoordelijk is voor het verwijderen van het deeltje van het scherm. We maken een variabele genaamd _levenslang om het aantal milliseconden te bevatten dat het deeltje op het scherm zal staan. We initialiseren standaard de waarde tot 1000 op de constructor. De bijwerken() De functie wordt elk frame genoemd en ontvangt de hoeveelheid miliseconden die tussen frames is gepasseerd, zodat deze de levensduurwaarde van het deeltje kan verlagen. Wanneer deze waarde 0 of minder bereikt, vraagt ​​het deeltje zijn ouder om het van het scherm te verwijderen. De rest van de code zorgt voor de beweging van het deeltje.

Nu zullen we er een aantal maken, wanneer er een muisklik wordt gedetecteerd. Ga naar Main.as:

 private var _oldTime: uint; privé var _elapsed: uint; private function init (e: Event = null): void removeEventListener (Event.ADDED_TO_STAGE, init); // entry point stage.addEventListener (MouseEvent.CLICK, createParticles); addEventListener (Event.ENTER_FRAME, updateParticles); _oldTime = getTimer ();  update-onderdelen privéfunctie (e: Event): void _elapsed = getTimer () - _oldTime; _oldTime + = _elapsed; for (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));  

De code voor het bijwerken van de partikels zou u bekend moeten zijn: het is de oorsprong van een eenvoudige op tijd gebaseerde lus, vaak gebruikt in games. Vergeet de importstatements niet:

 import flash.events.Event; import flash.events.MouseEvent; import flash.utils.getTimer;

U kunt uw applicatie nu testen en profileren met behulp van de ingebouwde profiler van FlashDevelop. Klik een aantal keren op het scherm. Hier is hoe mijn geheugengebruik eruit zag:

Ik klikte totdat de vuilnisman begon te rennen. De toepassing creëerde meer dan 2000 deeltjes die werden verzameld. Begint het er uit te zien als het geheugengebruik van dat prototype? Het lijkt erop, en dit is absoluut niet goed. Om profilering eenvoudiger te maken, voegen we het hulpprogramma toe dat in de eerste stap werd genoemd. Hier is de code om toe te voegen in Main.as:

 private function init (e: Event = null): void removeEventListener (Event.ADDED_TO_STAGE, init); // entry point stage.addEventListener (MouseEvent.CLICK, createParticles); addEventListener (Event.ENTER_FRAME, updateParticles); addChild (nieuwe statistieken ()); _oldTime = getTimer (); 

Vergeet niet te importeren net.hires.debug.Stats en het is klaar om gebruikt te worden!


Stap 5: Een poolbaar object definiëren

De applicatie die we in stap 4 bouwden, was vrij eenvoudig. Het bevatte slechts een eenvoudig deeltje-effect, maar creëerde veel problemen in het geheugen. In deze stap gaan we aan een objectpool werken om dat probleem op te lossen.

Onze eerste stap naar een goede oplossing is na te denken over hoe de objecten zonder problemen kunnen worden gepoold. In een objectgroep moeten we er altijd voor zorgen dat het gecreëerde object gebruiksklaar is en dat het geretourneerde object volledig "geïsoleerd" is van de rest van de toepassing (dat wil zeggen geen verwijzingen naar andere dingen bevat). Om elk gepoold object te forceren om dat te kunnen doen, gaan we een creëren interface. Deze interface definieert twee belangrijke functies die het object moet hebben: vernieuwen() en vernietigen(). Op die manier kunnen we altijd die methoden bellen zonder zich zorgen te hoeven maken of het object ze heeft of niet (omdat het zal hebben). Dit betekent ook dat elk object dat we willen poolen deze interface moet implementeren. Dus hier is het:

 package public interface IPoolable function get destroyed (): Boolean; function renew (): void; function destroy (): void; 

Omdat onze deeltjes poolbaar zijn, moeten we ze laten implementeren IPoolable. In principe verplaatsen we alle code van hun constructeurs naar de vernieuwen() functie, en elimineer externe verwijzingen naar het object in de vernietigen() functie. Hier is hoe het eruit zou moeten zien:

 / * INTERFACE IPoolable * / public function get destroyed (): Boolean return _destroyed;  public function 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; _snelheid = 150; // Pixels per seconde _lifeTime = 1000; // Miliseconds public function destroy (): void if (_destroyed) return;  _destroyed = true; graphics.clear (); 

De constructor zou ook geen argumenten meer nodig hebben. Als u informatie aan het object wilt doorgeven, moet u het nu via functies doen. Vanwege de manier waarop het vernieuwen() functie werkt nu, we moeten ook instellen _vernietigd naar waar in de constructor, zodat de functie kan worden uitgevoerd.

Daarmee hebben we onze Deeltje klasse om zich te gedragen als een IPoolable. Op die manier kan de objectgroep een pool van deeltjes creëren.


Stap 6: De objectpool starten

Het is nu tijd om een ​​flexibele objectpool te maken die elk object kan poolen dat we willen. Deze pool zal een beetje op een fabriek lijken: in plaats van de nieuwe zoekwoord om objecten te maken die u kunt gebruiken, zullen we in plaats daarvan een methode in de pool aanroepen die een object aan ons retourneert.

Voor de eenvoud is de objectpool een Singleton. Op die manier hebben we overal toegang tot onze code. Begin met het maken van een nieuwe klasse genaamd "ObjectPool" en voeg de code toe om er een Singleton van te maken:

 pakket public class ObjectPool private static var _instance: ObjectPool; private static var _allowInstantiation: Boolean; public static function get instance (): ObjectPool if (! _instance) _allowInstantiation = true; _instance = nieuwe ObjectPool (); _allowInstantiation = false;  return _instance;  openbare functie ObjectPool () if (! _allowInstantiation) gooi nieuwe fout ("Probeert een singleton te instantiëren!"); 

De variabele _allowInstantiation is de kern van deze Singleton-implementatie: het is privé, dus alleen de eigen klasse kan wijzigen en de enige plaats waar het moet worden gewijzigd, is voordat het de eerste instantie ervan maakt.

We moeten nu beslissen hoe we de zwembaden binnen deze klasse moeten houden. Omdat het globaal is (d.w.z. elk object in uw toepassing kan poolen), moeten we eerst een manier verzinnen om altijd een unieke naam voor elke pool te hebben. Hoe doe je dat? Er zijn veel manieren, maar de beste die ik tot nu toe heb gevonden, is om de eigen klassennamen van de objecten als de naam van de pool te gebruiken. Op die manier kunnen we een pool met "deeltjes", een pool "Vijand" enzovoort hebben ... maar er is nog een probleem. Klassenamen hoeven alleen binnen hun pakketten uniek te zijn, dus bijvoorbeeld een klasse "BaseObject" binnen het pakket "vijanden" en een klasse "BaseObject" binnen het pakket "structures" is toegestaan. Dat zou problemen veroorzaken in het zwembad.

Het idee om klassenamen te gebruiken als ID's voor de pools is nog steeds geweldig, en dit is waar flash.utils.getQualifiedClassName () komt om ons te helpen. In principe genereert deze functie een string met de volledige klassenaam, inclusief alle pakketten. Dus nu kunnen we de gekwalificeerde klassenaam van elk object gebruiken als ID voor hun respectieve pools! Dit is wat we in de volgende stap zullen toevoegen.


Stap 7: Pools maken

Nu we een manier hebben om pools te identificeren, is het tijd om de code toe te voegen die ze maakt. Onze objectpool moet flexibel genoeg zijn om zowel statische als dynamische pools te ondersteunen (we hebben erover gesproken in stap 3, weet je nog?). We moeten ook de grootte van elke pool en het aantal actieve objecten in elke pool kunnen opslaan. Een goede oplossing hiervoor is om een ​​privéklasse te maken met al deze informatie en alle pools binnen een Voorwerp:

 pakket public class ObjectPool private static var _instance: ObjectPool; private static var _allowInstantiation: Boolean; private var _pools: Object; public static function get instance (): ObjectPool if (! _instance) _allowInstantiation = true; _instance = nieuwe ObjectPool (); _allowInstantiation = false;  return _instance;  openbare functie ObjectPool () if (! _allowInstantiation) gooi nieuwe fout ("Probeert een singleton te instantiëren!");  _pools = ;  class PoolInfo public var items: Vector.; public var itemClass: Class; public var size: uint; public var active: uint; public var isDynamic: Boolean; openbare functie PoolInfo (itemClass: Class, size: uint, isDynamic: Boolean = true) this.itemClass = itemClass; items = nieuwe Vector.(size,! isDynamic); this.size = grootte; this.isDynamic = isDynamic; actief = 0; initialiseren ();  private function initialize (): void for (var i: int = 0; i < size; i++)  items[i] = new itemClass();   

De bovenstaande code maakt de privéklasse die alle informatie over een pool bevat. We hebben ook het _pools object om alle objectpools te bevatten. Hieronder zullen we de functie aanmaken die een pool registreert in de klas:

 public function registerPool (objectClass: Class, size: uint = 1, isDynamic: Boolean = true): void if (! (describeType (objectClass) .factory.implementsInterface. (@ type == "IPoolable"). length ()> 0)) gooi nieuwe fout weg ("Kan niet iets samenvoegen dat geen IPoolable implementeert!"); terug te keren;  var qualifiedName: String = getQualifiedClassName (objectClass); if (! _pools [qualifiedName]) _pools [qualifiedName] = nieuwe PoolInfo (objectClass, size, isDynamic); 

Deze code ziet er wat lastiger uit, maar raak niet in paniek. Het wordt hier allemaal uitgelegd. De eerste als verklaring ziet er echt raar uit. Je hebt deze functies misschien nog nooit eerder gezien, dus hier is wat het doet:

  • De functie describeType () maakt een XML die alle informatie bevat over het object dat we hebben gepasseerd.
  • In het geval van een klas zit alles erover in de fabriek label.
  • Daarbinnen beschrijft de XML alle interfaces die de klasse implementeert met de implementsInterface label.
  • We doen een snel onderzoek om te zien of het IPoolable interface is onder hen. Als dat zo is, weten we dat we die klasse aan de pool kunnen toevoegen, omdat we deze met succes als een klasse kunnen casten Ik protesteer.

De code na deze controle maakt gewoon een item in _pools als er nog geen bestond. Daarna, de PoolInfo constructor noemt het initialiseren () functie binnen die klasse, waardoor het zwembad effectief wordt gemaakt met de gewenste afmeting. Het is nu klaar om te worden gebruikt!


Stap 8: Een object krijgen

In de laatste stap konden we de functie maken die een objectpool registreert, maar nu moeten we een object krijgen om het te kunnen gebruiken. Het is heel eenvoudig: we krijgen een object als het zwembad niet leeg is en het teruggeven. Als het zwembad leeg is, controleren we of het dynamisch is; in dat geval vergroten we de grootte en maken we een nieuw object en retourneren. Zo niet, dan komen we null terug. (U kunt er ook voor kiezen om een ​​fout te genereren, maar het is beter om alleen null te retourneren en uw code in deze situatie te laten werken wanneer dit gebeurt.)

Hier is de getObj () functie:

 openbare functie getObj (objectClass: Class): IPoolable var qualifiedName: String = getQualifiedClassName (objectClass); if (! _pools [qualifiedName]) throw new Error ("Kan geen object ophalen van een pool die niet is geregistreerd!"); terug te keren;  var returnObj: IPoolable; if (PoolInfo (_pools [qualifiedName]). active == PoolInfo (_pools [gekwalificeerde naam]). size) if (PoolInfo (_pools [gekwalificeerde naam]). isDynamic) returnObj = new objectClass (); PoolInfo (_pools [qualifiedName]) formaat ++.; PoolInfo (_pools [qualifiedName]) items.push (returnObj).;  else return null;  else returnObj = PoolInfo (_pools [gekwalificeerde naam]). items [PoolInfo (_pools [gekwalificeerde naam]). actief]; returnObj.renew ();  PoolInfo (_pools [gekwalificeerde naam]). Actieve ++; return returnObj; 

In de functie controleren we eerst of de pool daadwerkelijk bestaat. Ervan uitgaande dat aan die voorwaarde is voldaan, controleren we of de pool leeg is: als dat zo is, maar het is dynamisch, maken we een nieuw object en voegen het toe aan de pool. Als het zwembad niet dynamisch is, stoppen we de code daar en keren we gewoon terug naar nul. Als het zwembad nog steeds een object heeft, krijgen we het object dat zich het dichtst bij het begin van het zwembad bevindt en belt het vernieuwen() ben ermee bezig. Dit is belangrijk: de reden die we noemen vernieuwen() op een object dat al in de pool zat, is om te garanderen dat dit object in een "bruikbare" staat wordt gegeven.

Je vraagt ​​je waarschijnlijk af: waarom gebruik je die coole cheque ook niet met describeType () in deze functie? Nou, het antwoord is simpel: describeType () maakt een XML elk tijd die we het noemen, dus het is erg belangrijk om te voorkomen dat objecten worden gemaakt die veel geheugen gebruiken en die we niet kunnen controleren. Bovendien is alleen controle om te zien of de pool echt bestaat voldoende: als de klasse voorbij is, implementeert deze niet IPoolable, dat betekent dat we er zelfs geen pool van kunnen maken. Als er geen zwembad voor is, dan vangen we zeker deze zaak in onze als verklaring aan het begin van de functie.

We kunnen nu onze wijzigen Hoofd klasse en gebruik de objectpool! Bekijken:

 private function init (e: Event = null): void removeEventListener (Event.ADDED_TO_STAGE, init); // entry point stage.addEventListener (MouseEvent.CLICK, createParticles); addEventListener (Event.ENTER_FRAME, updateParticles); _oldTime = getTimer (); ObjectPool.instance.registerPool (Particle, 200, true);  private function createParticles (e: MouseEvent): void var tempParticle: Particle; for (var i: int = 0; i < 10; i++)  tempParticle = ObjectPool.instance.getObj(Particle) as Particle; tempParticle.x = e.stageX; tempParticle.y = e.stageY; addChild(tempParticle);  

Druk compileer en profileer het geheugengebruik! Dit is wat ik heb:

Dat is best cool, toch??


Stap 9: Objecten terugsturen naar de pool

We hebben met succes een objectpool geïmplementeerd die ons objecten oplevert. Dat is geweldig! Maar het is nog niet voorbij. We krijgen nog steeds alleen objecten, maar geven deze nooit terug als we ze niet meer nodig hebben. Tijd om een ​​functie toe te voegen om objecten binnen te zetten ObjectPool.as:

 public function returnObj (obj: IPoolable): void var qualifiedName: String = getQualifiedClassName (obj); if (! _pools [qualifiedName]) throw new Error ("Kan een object niet retourneren uit een pool die niet is geregistreerd!"); terug te keren;  var objIndex: int = PoolInfo (_pools [qualifiedName]). items.indexOf (obj); if (objIndex> = 0) if (! PoolInfo (_pools [gekwalificeerde naam]). isDynamic) PoolInfo (_pools [gekwalificeerde naam]). items.fixed = false;  PoolInfo (_pools [qualifiedName]). Items.splice (objIndex, 1); obj.destroy (); PoolInfo (_pools [qualifiedName]) items.push (obj).; if (! PoolInfo (_pools [gekwalificeerde naam]). isDynamic) PoolInfo (_pools [gekwalificeerde naam]). items.fixed = true;  PoolInfo (_pools [gekwalificeerde naam]). Actief--; 

Laten we door de functie gaan: het eerste is om te controleren of er een pool is van het object dat is gepasseerd. Je bent die code gewend - het enige verschil is dat we nu een object gebruiken in plaats van een klasse om de gekwalificeerde naam te krijgen, maar dat verandert de uitvoer niet).

Vervolgens krijgen we de index van het artikel in de pool. Als het niet in de pool is, negeren we het gewoon. Nadat we hebben gecontroleerd of het object zich in de pool bevindt, moeten we de pool breken waar het object zich momenteel bevindt en het object aan het einde opnieuw plaatsen. En waarom? Omdat we de gebruikte objecten vanaf het begin van de pool tellen, moeten we de pool reorganiseren om ervoor te zorgen dat alle geretourneerde en ongebruikte objecten aan het einde ervan zijn. En dat is wat we doen in deze functie.

Voor statische objectgroepen maken we een Vector object met een vaste lengte. Daarom kunnen we dat niet splice () het en Duwen() voorwerpen terug. De oplossing hiervoor is het wijzigen van de vast eigendom van die Vectors tot vals, verwijder het object en voeg het terug aan het einde en verander de eigenschap terug naar waar. We moeten ook het aantal actieve objecten verminderen. Daarna zijn we klaar met het retourneren van het object.

Nu we de code hebben gemaakt om een ​​voorwerp te retourneren, kunnen we onze deeltjes terugbrengen naar het zwembad zodra ze het einde van hun leven hebben bereikt. Binnen Particle.as:

 update van publieke functie (timePassed: uint): void // Deelbeweging maken x + = Math.cos (_angle) * _speed * timePassed / 1000; y + = Math.sin (_angle) * _speed * timePassed / 1000; // Kleine versnelling om de beweging er mooi uit te laten zien _snelheid - = 120 * timePassed / 1000; // Verzorgen van de levensduur en verwijderen _lifeTime - = timePassed; if (_lifeTime <= 0)  parent.removeChild(this); ObjectPool.instance.returnObj(this);  

Merk op dat we een oproep hebben toegevoegd aan ObjectPool.instance.returnObj () daarin. Dat is wat het object doet terugkeren naar het zwembad. We kunnen onze app nu testen en profileren:

En daar gaan we! Stabiel geheugen, zelfs wanneer honderden klikken zijn gemaakt!


Conclusie

U weet nu hoe u een objectgroep maakt en gebruikt om het geheugengebruik van uw app stabiel te houden. De klasse die we hebben gebouwd, kan overal worden gebruikt en het is heel eenvoudig om je code hieraan aan te passen: maak aan het begin van je app objectgroepen voor elk soort object dat je wilt poolen, en wanneer er een nieuwe zoekwoord (dit betekent het maken van een instantie), vervang het door een aanroep naar de functie die een object voor u krijgt. Vergeet niet om de methoden die de interface gebruikt te implementeren IPoolable vereist!

Het is erg belangrijk dat u uw geheugengebruik stabiel houdt. Het bespaart je een hoop problemen later in je project wanneer alles uit elkaar valt met niet-gerepliceerde instanties die nog steeds reageren op gebeurtenislisteners, objecten die het geheugen vullen dat je beschikbaar hebt om te gebruiken en met de garbagecollector die loopt en alles vertraagt. Een goede aanbeveling is om altijd objectpooling te gebruiken en u zult merken dat uw leven veel eenvoudiger zal zijn.

Merk ook op dat hoewel deze zelfstudie bedoeld was voor Flash, de concepten die erin zijn ontwikkeld, globaal zijn: u kunt deze gebruiken op AIR-apps, mobiele apps en overal waar deze past. Bedankt voor het lezen!