Hold Flash-prosjektets minnebruk stabilt med objektbassering

Minnebruk er et aspekt av utvikling som du virkelig må være forsiktig med, eller det kan ende opp med å bremse appen din, ta opp mye minne eller krasje alt. Denne opplæringen vil hjelpe deg med å unngå de dårlige potensielle resultatene!


Endelig resultatforhåndsvisning

La oss se på det endelige resultatet vi vil jobbe for:

Klikk hvor som helst på scenen for å opprette en fyrverkeri effekt, og hold øye på minnesprofilen øverst til venstre.


Trinn 1: Introduksjon

Hvis du noen gang har profilert søknaden din ved hjelp av et profileringsverktøy eller brukt en kode eller et bibliotek som forteller deg gjeldende minnebruk av søknaden din, har du kanskje lagt merke til at mange ganger går minnemengden opp, og deretter går ned igjen (hvis du har havn ikke koden din er fantastisk!). Vel, selv om disse pigger forårsaket av stor minnebruk ser litt kult ut, er det ikke gode nyheter for enten søknaden din eller (følgelig) brukerne dine. Fortsett å lese for å forstå hvorfor dette skjer og hvordan du kan unngå det.


Trinn 2: God og dårlig bruk

Bildet nedenfor er et veldig godt eksempel på dårlig minnehåndtering. Det er fra en prototype av et spill. Du må legge merke til to viktige ting: de store pigger på minnebruk og minnebrukstoppen. Toppet er nesten på 540MB! Det betyr at denne prototypen alene kom til å benytte 540Mb av brukerens datamaskin RAM - og det er noe du definitivt vil unngå.

Dette problemet begynner når du begynner å lage mange objektfelter i applikasjonen din. Ubrukte forekomster vil fortsette å bruke programmets minne til søppelkollektor løper, når de blir deallokert - forårsaker de store pigger. En enda verre situasjon skjer når instansene rett og slett ikke blir deallokated, noe som gjør at programmets minnebruk forblir voksende til noe krasjer eller bryter. Hvis du vil vite mer om det sistnevnte problemet, og hvordan du unngår det, kan du lese denne raske tipsen om søppelsamling.

I denne opplæringen vil vi ikke ta opp eventuelle søppelkollektorproblemer. Vi vil i stedet arbeide med å bygge strukturer som effektivt beholder gjenstander i minnet, slik at bruken blir helt stabil og dermed holder søppelkollektoren fra å rydde opp minnet, noe som gjør søknaden raskere. Ta en titt på minnebruk av samme prototype over, men denne gangen optimalisert med teknikkene som vises her:

Alt denne forbedringen kan oppnås ved hjelp av objektbassing. Les videre for å forstå hva det er og hvordan det fungerer.


Trinn 3: Typer av bassenger

Objektpooling er en teknikk hvor et forhåndsdefinert antall objekter blir opprettet når applikasjonen initialiseres og holdes i minnet i hele programmets levetid. Objektpolen gir objekter når søknaden ber om dem, og tilbakestiller gjenstandene tilbake til opprinnelig tilstand når programmet er ferdig med å bruke dem. Det er mange typer objektpuljer, men vi vil bare se på to av dem: det statiske og det dynamiske objektet bassenger.

Den statiske objektbassenget oppretter et definert antall objekter, og beholder bare den mengden objekter i hele programmets levetid. Hvis et objekt blir bedt om, men bassenget har allerede gitt alle sine objekter, returnerer bassenget null. Når du bruker denne typen basseng, er det nødvendig å ta opp problemer som å be om et objekt og ikke få noe tilbake.

Det dynamiske objektbassenget oppretter også et definert antall objekter ved initialisering, men når et objekt blir forespurt og bassenget er tomt, oppretter bassenget en annen forekomst automatisk og returnerer det objektet, øker bassengstørrelsen og legger til det nye objektet.

I denne opplæringen vil vi bygge et enkelt program som genererer partikler når brukeren klikker på skjermen. Disse partiklene vil ha en endelig levetid, og da vil de bli fjernet fra skjermen og returnert til bassenget. For å gjøre det, vil vi først opprette dette programmet uten objektbassing og sjekke minnebruken, og deretter implementere objektbassenget og sammenligne minnebruken til før.


Trinn 4: Førstegangsapplikasjon

Åpne FlashDevelop (se denne håndboken) og opprett et nytt AS3-prosjekt. Vi vil bruke et enkelt lite farget firkant som partikkelbildet, som vil bli tegnet med kode og vil bevege seg i henhold til en tilfeldig vinkel. Opprett en ny klasse kalt partikkel som strekker seg til Sprite. Jeg antar at du kan håndtere opprettelsen av en partikkel, og bare markere aspektene som vil holde styr på partikkelsens levetid og fjerning fra skjermen. Du kan hente full kildekoden til denne opplæringen øverst på siden hvis du har problemer med å lage partikkelen.

 privat var _lifeTime: int; offentlig funksjon oppdatering (timePassed: uint): void // Gjøre partikkelflytten x + = Math.cos (_angle) * _speed * timePassed / 1000; y + = Math.sin (_angle) * _speed * timePassed / 1000; // Smal lettelse for å gjøre bevegelsen ser pen ut _speed - = 120 * timePassed / 1000; // Ta vare på levetid og fjerning _lifeTime - = timePassed; hvis (_lifeTime <= 0)  parent.removeChild(this);  

Koden ovenfor er koden som er ansvarlig for partikkelsens fjerning fra skjermen. Vi lager en variabel som heter _livstid å inneholde antall milisekunder at partikkelen vil være på skjermen. Vi initialiserer som standard sin verdi til 1000 på konstruktøren. De Oppdater() funksjon kalles hver ramme og mottar mengden milisekunder som passerte mellom rammer, slik at den kan redusere partikkelens levetidsverdi. Når denne verdien når 0 eller mindre, spør partiklen automatisk at dennes foreldre fjerner den fra skjermen. Resten av koden tar vare på partikkels bevegelse.

Nå skal vi lage en haug med disse bli opprettet når et museklikk oppdages. Gå til Main.as:

 privat var _oldTime: uint; privat var _lapsed: uint; privat funksjon 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 ();  privat funksjon updateParticles (e: Event): void _elapsed = getTimer () - _oldTime; _oldTime + = _elapsed; for (var jeg: 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));  

Koden for oppdatering av partiklene skal være kjent for deg: det er røttene til en enkel tidsbasert sløyfe, ofte brukt i spill. Ikke glem importinnstillingene:

 importere flash.events.Event; importer flash.events.MouseEvent; importer flash.utils.getTimer;

Du kan nå teste din søknad og profilere den ved hjelp av FlashDevelops innebygde profiler. Klikk en masse ganger på skjermen. Her ser min minnebruk ut:

Jeg klikket til søppelsamleren begynte å løpe. Søknaden opprettet over 2000 partikler som ble samlet inn. Er det begynt å se ut som minnet bruken av prototypen? Det ser ut som det, og dette er definitivt ikke bra. For å gjøre profilering enklere, legger vi til verktøyet som ble nevnt i første trinn. Her er koden du vil legge til i Main.as:

 privat funksjon init (e: Event = null): void removeEventListener (Event.ADDED_TO_STAGE, init); // entry point stage.addEventListener (MouseEvent.CLICK, createParticles); addEventListener (Event.ENTER_FRAME, updateParticles); addChild (nye Stats ()); _oldTime = getTimer (); 

Ikke glem å importere net.hires.debug.Stats og den er klar til bruk!


Trinn 5: Definere et Poolable Object

Programmet vi bygde i trinn 4 var ganske enkelt. Den inneholdt bare en enkel partikkel-effekt, men skapte mye problemer i minnet. I dette trinnet begynner vi å jobbe med en objektbasseng for å fikse dette problemet.

Vårt første skritt mot en god løsning er å tenke på hvordan objekter kan samles uten problemer. I et objektbasseng må vi alltid sørge for at objektet som er opprettet, er klar til bruk, og at gjenstanden som returneres, er helt "isolert" fra resten av programmet (det vil si at det ikke er noen referanser til andre ting). For å tvinge hvert samlet objekt til å kunne gjøre det, skal vi lage en grensesnitt. Dette grensesnittet definerer to viktige funksjoner som objektet må ha: fornye() og ødelegge(). På den måten kan vi alltid ringe til disse metodene uten å bekymre oss om hvorvidt objektet har dem (fordi det vil ha) eller ikke. Dette betyr også at hvert objekt vi ønsker å slå sammen, må implementere dette grensesnittet. Så her er det:

 pakke offentlig grensesnitt IPoolable funksjon bli ødelagt (): boolsk; funksjon forny (): void; funksjon ødelegge (): void; 

Siden partiklene våre vil bli poolbare, må vi få dem til å gjennomføre IPoolable. I utgangspunktet flytter vi all koden fra deres konstruktører til fornye() funksjon, og fjern eventuelle eksterne referanser til objektet i ødelegge() funksjon. Slik ser det ut:

 / * INTERFACE IPoolable * / offentlig funksjon bli ødelagt (): Boolean return _destroyed;  offentlig funksjon forny (): void if (! _destroyed) return;  _destroyed = false; grafikk.beginFill (uint (Math.random () * 0xFFFFFF), 0,5 + (Math.random () * 0,5)); grafikk.drawRekt (-1,5, -1,5, 3, 3); graphics.endFill (); _angle = Math.random () * Math.PI * 2; _speed = 150; // Pixels per sekund _lifeTime = 1000; // Miliseconds offentlig funksjon ødelegge (): void if (_destroyed) return;  _destroyed = true; graphics.clear (); 

Konstruktøren bør heller ikke kreve noen argumenter lenger. Hvis du vil overføre informasjon til objektet, må du gjøre det gjennom funksjoner nå. På grunn av måten som fornye() funksjonen fungerer nå, vi må også sette _destroyed til ekte i konstruktøren slik at funksjonen kan kjøres.

Med det har vi nettopp tilpasset vår partikkel~~POS=TRUNC klasse å oppføre seg som en IPoolable. På den måten vil objektbassenget kunne opprette et partikkelparti.


Trinn 6: Start objektbassenget

Det er på tide å lage en fleksibel objektbasseng som kan samle alle gjenstander vi ønsker. Dette bassenget vil virke litt som en fabrikk: i stedet for å bruke ny søkeord for å lage objekter du kan bruke, vil vi i stedet kalle en metode i bassenget som returnerer til oss et objekt.

For enkelhets skyld vil objektbassenget være en Singleton. På den måten kan vi få tilgang til den hvor som helst i vår kode. Start med å lage en ny klasse kalt "ObjectPool" og legge til koden for å gjøre det til en Singleton:

 pakke public class ObjectPool private static var _instance: ObjectPool; privat statisk var _allowInstantiation: boolsk; offentlig statisk funksjon få forekomst (): ObjectPool if (! _instance) _allowInstantiation = true; _instance = nytt ObjectPool (); _allowInstantiation = false;  returnere _instance;  offentlig funksjon ObjectPool () if (! _allowInstantiation) kaste ny feil ("Prøver å instansere en Singleton!"); 

Variabelen _allowInstantiation er kjernen i denne Singleton-implementeringen: den er privat, så bare den egen klassen kan modifisere, og det eneste stedet der det skal endres, er før du lager den første forekomsten av det.

Vi må nå bestemme hvordan vi skal holde bassengene inne i denne klassen. Siden det blir globalt (det vil si at det kan være noe objekt i søknaden din), må vi først komme opp på en måte å alltid ha et unikt navn for hvert basseng. Hvordan gjøre det? Det er mange måter, men det beste jeg hittil har funnet er å bruke objektets egne klassenavn som bassengnavn. På den måten kunne vi ha et "Particle" -basseng, et "Enemy" -basseng og så videre ... men det er et annet problem. Klassenavn må bare være unike innenfor sine pakker, for eksempel kan en klasse "BaseObject" i "fiender" -pakken og en klasse "BaseObject" i "strukturer" -pakken bli tillatt. Det ville føre til problemer i bassenget.

Ideen om å bruke klassenavn som identifikatorer for bassengene er fortsatt stor, og det er her flash.utils.getQualifiedClassName () kommer til å hjelpe oss. I utgangspunktet genererer denne funksjonen en streng med hele klassenavnet, inkludert eventuelle pakker. Så nå kan vi bruke hvert objekts kvalifiserte klassenavn som identifikator for deres respektive bassenger! Dette er hva vi legger til i neste trinn.


Trinn 7: Opprette bassenger

Nå som vi har en måte å identifisere bassenger på, er det på tide å legge til koden som lager dem. Vårt objekt basseng bør være fleksibelt nok til å støtte både statiske og dynamiske bassenger (vi snakket om disse i trinn 3, husk?). Vi må også kunne lagre størrelsen på hvert basseng og hvor mange aktive objekter det er i hver enkelt. En god løsning for det er å lage en privat klasse med all denne informasjonen og lagre alle bassenger innenfor en Gjenstand:

 pakke public class ObjectPool private static var _instance: ObjectPool; privat statisk var _allowInstantiation: boolsk; private var _pools: Objekt; offentlig statisk funksjon få forekomst (): ObjectPool if (! _instance) _allowInstantiation = true; _instance = nytt ObjectPool (); _allowInstantiation = false;  returnere _instance;  offentlig funksjon ObjectPool () if (! _allowInstantiation) kaste ny feil ("Prøver å instansere en Singleton!");  _pools = ;  klasse PoolInfo offentlige varelementer: Vector.; offentlig var vareklasse: klasse; offentlig var størrelse: uint; offentlig var aktiv: uint; offentlig var isDynamisk: boolsk; offentlig funksjon PoolInfo (itemClass: Klasse, størrelse: uint, isDynamic: Boolean = true) this.itemClass = itemClass; elementer = ny vektor.(størrelse,! erDynamisk); this.size = size; this.isDynamic = isDynamic; aktiv = 0; initialisere ();  privat funksjon initialisere (): void for (var i: int = 0; i < size; i++)  items[i] = new itemClass();   

Koden ovenfor skaper den private klassen som vil inneholde all informasjon om et basseng. Vi har også opprettet _pools gjenstand for å holde alle objektbassenger. Nedenfor vil vi opprette funksjonen som registrerer et basseng i klassen:

 offentlig funksjon registerPool (objectClass: Klasse, størrelse: uint = 1, erDynamisk: Boolean = true): void if (! (describeType (objectClass) .factory.implementsInterface. (@ type == "IPoolable"). 0)) Kast ny feil ("Kan ikke samle noe som ikke implementerer IPoolable!"); komme tilbake;  var kvalifisertName: String = getQualifiedClassName (objectClass); hvis (! _pools [qualifiedName]) _pools [qualifiedName] = ny PoolInfo (objectClass, size, isDynamic); 

Denne koden ser litt vanskeligere ut, men ikke panikk. Det er alt forklart her. Den første hvis setningen ser veldig rar ut. Du har kanskje aldri sett disse funksjonene før, så her er hva det gjør:

  • FunctionType () -funksjonen oppretter et XML som inneholder all informasjon om objektet vi passerte den.
  • I tilfelle av en klasse, er alt om det inneholdt i fabrikk stikkord.
  • Innenfor, beskriver XML alle grensesnittene som klassen implementerer med implementsInterface stikkord.
  • Vi gjør et raskt søk for å se om IPoolable grensesnittet er blant dem. I så fall vet vi at vi kan legge den klassen til bassenget, fordi vi vil kunne klare det som en Jeg protesterer.

Koden etter denne sjekken oppretter bare en oppføring innenfor _pools hvis man ikke allerede eksisterte. Etter det, PoolInfo konstruktør kaller initialize () fungere innenfor den klassen, og effektivt skape bassenget med den størrelsen vi ønsker. Den er nå klar til bruk!


Trinn 8: Få et objekt

I det siste trinnet var vi i stand til å opprette funksjonen som registrerer et objektbasseng, men nå må vi få et objekt for å kunne bruke det. Det er veldig greit: vi får et objekt hvis bassenget ikke er tomt og returnerer det. Hvis bassenget er tomt, kontrollerer vi om det er dynamisk; Hvis ja, øker vi størrelsen, og oppretter deretter et nytt objekt og returnerer det. Hvis ikke, returnerer vi null. (Du kan også velge å kaste en feil, men det er bedre å bare returnere null og få koden til å fungere rundt denne situasjonen når det skjer.)

Her er getObj () funksjon:

 offentlig funksjon getObj (objectClass: Class): IPoolable var kvalifisertName: String = getQualifiedClassName (objectClass); hvis (! _pools [qualifiedName]) kaste ny feil ("Kan ikke få et objekt fra et basseng som ikke er registrert!"); komme tilbake;  var returnObj: IPoolable; hvis (PoolInfo (_pools [qualifiedName]) .aktiv == PoolInfo (_pools [qualifiedName]) .størrelse) if (PoolInfo (_pools [qualifiedName]) .Dynamic) returnObj = new objectClass (); PoolInfo (_pools [qualifiedName]) størrelse ++.; PoolInfo (_pools [qualifiedName]) items.push (returnObj.);  ellers return null;  else returnObj = PoolInfo (_pools [qualifiedName]). elementer [PoolInfo (_pools [qualifiedName]). aktiv]; returnObj.renew ();  PoolInfo (_pools [qualifiedName]). Aktiv ++; returnere tilbakeObj; 

I funksjonen kontrollerer vi først at bassenget faktisk eksisterer. Forutsatt at tilstanden er oppfylt, kontrollerer vi om bassenget er tomt: hvis det er, men det er dynamisk, oppretter vi et nytt objekt og legger til i bassenget. Hvis bassenget ikke er dynamisk, stopper vi koden der og bare returnerer null. Hvis bassenget fortsatt har en gjenstand, får vi objektet nærmest begynnelsen av bassenget og ring fornye() på den. Dette er viktig: grunnen vi kaller fornye() på et objekt som allerede var i bassenget, er å garantere at dette objektet vil bli gitt i en "brukbar" tilstand.

Du lurer sikkert på: hvorfor bruker du heller ikke den kule sjekken med describeType () i denne funksjonen? Vel, svaret er enkelt: describeType () lager en XML hver tid vi kaller det, så det er veldig viktig å unngå å lage objekter som bruker mye minne og at vi ikke kan kontrollere. Dessuten er det bare å sjekke om bassenget egentlig eksisterer: hvis klassen passert ikke implementeres IPoolable, det betyr at vi ikke engang kunne lage et basseng for det. Hvis det ikke er et basseng for det, så får vi definitivt dette tilfellet i vår hvis setning i begynnelsen av funksjonen.

Vi kan nå endre vår Hoved klasse og bruk objektet bassenget! Sjekk det ut:

 privat funksjon 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);  privat funksjon createParticles (e: MouseEvent): void var tempParticle: Particle; for (var jeg: int = 0; i < 10; i++)  tempParticle = ObjectPool.instance.getObj(Particle) as Particle; tempParticle.x = e.stageX; tempParticle.y = e.stageY; addChild(tempParticle);  

Hit kompilere og profil minnebruk! Her er hva jeg fikk:

Det er litt kult, er det ikke?


Trinn 9: Gjenoppretter gjenstander til bassenget

Vi har vellykket implementert et objektbasseng som gir oss objekter. Det er utrolig! Men det er ikke over ennå. Vi får fremdeles bare objekter, men returnerer dem aldri når vi ikke trenger dem lenger. Tid til å legge til en funksjon for å returnere objekter inne ObjectPool.as:

 offentlig funksjon returnObj (obj: IPoolable): void var kvalifisertName: String = getQualifiedClassName (obj); hvis (! _pools [qualifiedName]) kaste ny feil ("Kan ikke returnere en gjenstand fra et basseng som ikke er registrert!"); komme tilbake;  var objIndex: int = PoolInfo (_pools [qualifiedName]). items.indexOf (obj); hvis (objIndex> = 0) hvis (! PoolInfo (_pools [qualifiedName]) .Dynamisk) PoolInfo (_pools [qualifiedName]). items.fixed = false;  PoolInfo (_pools [qualifiedName]). Items.splice (objIndex, 1); obj.destroy (); PoolInfo (_pools [qualifiedName]) items.push (obj.); hvis (! PoolInfo (_pools [qualifiedName]) .Dynamisk) PoolInfo (_pools [qualifiedName]). items.fixed = true;  PoolInfo (_pools [qualifiedName]). Active--; 

La oss gå gjennom funksjonen: Det første er å sjekke om det er et basseng av objektet som ble bestått. Du er vant til den koden - den eneste forskjellen er at vi nå bruker et objekt i stedet for en klasse for å få det kvalifiserte navnet, men det endrer ikke utgangen).

Deretter får vi indeksen for varen i bassenget. Hvis det ikke er i bassenget, ignorerer vi bare det. Når vi verifiserer at objektet er i bassenget, må vi bryte bassenget der objektet er for tiden, og sett inn objektet på nytt på slutten av det. Og hvorfor? Fordi vi teller de brukte gjenstandene fra begynnelsen av bassenget, må vi omorganisere bassenget for å få alle tilbake og ubrukte objekter til å være på slutten av det. Og det er det vi gjør i denne funksjonen.

For statiske objektbassenger lager vi en Vector objekt som har fast lengde. På grunn av det kan vi ikke spleise () det og trykk() gjenstander. Løsningen til dette er å endre fast eiendom av disse Vectors til falsk, fjern objektet og legg det tilbake på slutten, og endre deretter eiendommen tilbake til ekte. Vi må også redusere antall aktive objekter. Etter det er vi ferdige med å returnere objektet.

Nå som vi har opprettet koden for å returnere et objekt, kan vi få partiklene tilbake til bassenget når de når slutten av deres levetid. Innsiden Particle.as:

 offentlig funksjon oppdatering (timePassed: uint): void // Gjøre partikkelflytten x + = Math.cos (_angle) * _speed * timePassed / 1000; y + = Math.sin (_angle) * _speed * timePassed / 1000; // Smal lettelse for å gjøre bevegelsen ser pen ut _speed - = 120 * timePassed / 1000; // Ta vare på levetid og fjerning _lifeTime - = timePassed; hvis (_lifeTime <= 0)  parent.removeChild(this); ObjectPool.instance.returnObj(this);  

Legg merke til at vi har lagt til et anrop til ObjectPool.instance.returnObj () der inne Det er det som gjør objektet tilbake til bassenget. Vi kan nå teste og profilere vår app:

Og der går vi! Stabilt minne, selv om hundrevis av klikk ble laget!


Konklusjon

Du vet nå hvordan du oppretter og bruker et objektbasseng for å holde appens minnebruk stabil. Klassen vi bygget bygget kan brukes hvor som helst, og det er veldig enkelt å tilpasse koden til den: i begynnelsen av appen din, lager du objektpuljer for hver type objekt du vil boble, og når det er en ny søkeord (som betyr opprettelsen av en forekomst), erstatt den med et anrop til funksjonen som får et objekt for deg. Ikke glem å implementere metodene som grensesnittet IPoolable krever!

Å holde minneforbruket stabilt er veldig viktig. Det sparer deg mye trøbbel senere i prosjektet når alt begynner å falle fra hverandre med urecyklede forekomster som fortsatt reagerer på hendelseslyttere, objekter fylle opp minnet du har tilgjengelig for bruk og med søppelsamleren løper og bremser alt ned. En god anbefaling er å alltid bruke objektbassing fra nå av, og du vil legge merke til at livet ditt vil bli mye lettere.

Legg også merke til at selv om denne opplæringen var rettet mot Flash, er konseptene som er utviklet i det globale: du kan bruke den på AIR-apper, mobilapper og hvor som helst det passer. Takk for at du leste!