Keep Din Flash Prosjektets Minnebruk Stabil Med Object Samling
en
Del
35
Del
Dette Cyber mandag Envato Tuts + kurs vil bli redusert til bare $ 3. Ikke gå glipp av.
minnebruk er en del av utviklingen at du virkelig må være forsiktig om, eller det kan ende opp med å bremse ned appen, tar opp mye minne eller krasje alt. Denne opplæringen vil hjelpe deg å unngå de dårlige potensielle utfall
Endelig resultat Forhåndsvisning
La oss ta en titt på det endelige resultatet vi skal jobbe mot:
Klikk hvor som helst på scenen for å skape et fyrverkeri effekt, og holde et øye på minne profiler i øverste venstre hjørne
Trinn 1:. Innledning
Hvis du noensinne har profilert din søknad ved hjelp av noen profilering verktøy eller brukt noen kode eller biblioteket som forteller deg dagens minnebruken av søknaden din, har du kanskje lagt merke til at mange ganger minnebruken går opp, og deretter går ned igjen (hvis du ikke har, er koden superb!). Vel, selv om disse toppene forårsaket av store minnebruken ser ganske kult, det er ikke gode nyheter for enten programmet eller (følgelig) brukerne. Hold lesing for å forstå hvorfor dette skjer og hvordan du kan unngå det
Trinn 2:. Gode og dårlige Usage
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 minnebruken peak. Toppen er nesten på 540Mb! Det betyr at denne prototypen alene nådd poenget med å bruke 540Mb av brukerens datamaskin RAM - og det er noe du definitivt ønsker å unngå
Dette problemet begynner når du begynner å skape en masse objekt forekomster i programmet.. Ubrukte tilfeller vil fortsette å bruke programmets minnet inntil garbage collector går, når de får deallocated - forårsaker de store toppene. En enda verre situasjon som skjer når de tilfeller rett og slett ikke vil få deallocated, forårsaker minnebruk for programmet til å vokse til noe krasjer eller pauser. Hvis du ønsker å vite mer om det siste problemet og hvordan du kan unngå det, må du lese dette Quick Tips om søppelrydding.
I denne opplæringen vil vi ikke ta opp noen søppelinnsamler problemer. Vi vil i stedet jobbe med å bygge strukturer som effektivt holder gjenstander i minnet, noe som gjør bruken helt stabilt og dermed holde søppeltømmeren fra rydde opp minne, noe som gjør programmet raskere. Ta en titt på minnebruk av samme prototype ovenfor, men denne gangen optimalisert med de teknikkene som vises her:
Alt dette forbedring kan oppnås ved hjelp av objekt pooling. Les videre for å forstå hva det er og hvordan det fungerer
Trinn 3:. Typer Pools
Objekt pooling er en teknikk hvor en forhåndsdefinert antall objekter opprettes når søknad er initialisert, og holdes i minnet under hele programmet levetid. Objektet bassenget gir objektene når søknaden ber dem, og tilbakestiller gjenstandene tilbake til den opprinnelige tilstanden når søknaden er ferdig med å bruke dem. Det finnes mange typer objekt bassenger, men vi vil bare ta en titt på to av dem. Den statiske og dynamiske objekt bassenger
statisk objekt bassenget skaper et definert antall objekter og bare holder den mengden objekter under hele programmet levetid. Hvis et objekt er forespurt, men utvalget har allerede gitt alle sine objekter, bassenget returnerer null. Ved bruk av denne type basseng, er det nødvendig å ta opp spørsmål som ber om et objekt og får ingenting tilbake.
Den dynamiske objektet bassenget skaper også et definert antall objekter på initialisering, men når et objekt er forespurt og bassenget er tomt, bassenget skaper en annen forekomst automatisk og returnerer som objekt, øke størrelsen bassenget og legge det nye objektet til det.
I denne opplæringen vil vi bygge et enkelt program som genererer partikler når brukeren klikker på skjermen. Disse partikler vil ha en begrenset levetid, og deretter vil bli fjernet fra skjermen og tilbake til bassenget. For å gjøre det, vil vi først opprette dette programmet uten objekt pooling og sjekk minnebruk, og deretter implementere objektet bassenget og sammenligne minnebruken til før
Trinn 4:. Initial Application
Åpne FlashDevelop (se denne guiden) og opprette et nytt AS3 prosjekt. Vi bruker en enkel liten farget firkant som partikkelbildet, som vil bli trukket med kode og vil bevege seg i henhold til en tilfeldig vinkel. Opprett en ny klasse kalt Particle som strekker Sprite. Jeg vil anta at du kan håndtere etableringen av en partikkel, og bare fremheve de aspekter som vil holde styr på partikkelens levetid og fjerning fra skjermen. Du kan ta tak i hele kildekoden til denne opplæringen på toppen av siden hvis du har problemer med å opprette partikkel
private Var _lifeTime: int; offentlig funksjon oppdateringen (timePassed: uint):. Void {//Making partikkel flytte x + = Math.cos (_angle) * _speed * timePassed /1000; y + = tak i Math.sin (_angle) * _speed * timePassed /1000; //Liten lettelser for å gjøre bevegelsen ser ganske _speed - = 120 * timePassed /1000; //Å ta vare på livet og fjerning _lifeTime - = timePassed; if (_lifeTime < = 0) {parent.removeChild (denne); }}
Koden ovenfor er koden ansvarlig for partikkelens fjernet fra skjermen. Vi skape en variabel kalt _lifeTime å inneholde antallet miliseconds at partikkelen vil være på skjermen. Vi initial som standard verdien til 1000 på konstruktøren. Oppdateringen () -funksjonen kalles hver ramme og mottar mengden miliseconds som passerte mellom rammene, slik at det kan redusere partikkelens levetid verdi. Når denne verdien når 0 eller mindre, spør partikkel automatisk moder å fjerne det fra skjermen. Resten av koden tar seg av partikkelens bevegelse.
Nå skal vi lage en haug av disse lages når et museklikk er oppdaget. Gå til Main.as:
private Var _oldTime: uint; private Var _elapsed: uint; privat funksjon init (e: Hendelses = null): void {removeEventListener (Event.ADDED_TO_STAGE, init); //Inngangspunkt stage.addEventListener (MouseEvent.CLICK, createParticles); addEventListener (Event.ENTER_FRAME, updateParticles); _oldTime = getTimer ();} private funksjons updateParticles (e: Hendelses): void {_elapsed = getTimer () - _oldTime; _oldTime + = _elapsed; for (var i: int = 0; i < numChildren; i ++). {if (getChildAt (i) er Particle) {Partikkel (getChildAt (i)) oppdatering (_elapsed); }}} Private funksjons createParticles (e: MouseEvent): void {for (var i: int = 0; i < 10; i ++) {addChild (ny partikkel (stage.mouseX, stage.mouseY)); }}
Koden for å oppdatere partiklene bør være kjent for deg: det er røttene til en enkel tidsbasert sløyfe, som vanligvis brukes i spill. Ikke glem import uttalelser:
import flash.events.Event, import flash.events.MouseEvent, import flash.utils.getTimer;
Du kan nå teste din søknad og profilere den ved hjelp FlashDevelop innebygde profiler . Klikk på en haug med ganger på skjermen. Her er hva min minnebruk så ut:
Jeg klikket helt til søppelinnsamler begynte å løpe. Søknaden opprettet over 2000 partikler som ble samlet inn. Er det begynner å se ut minnebruken av at prototypen? Det ser ut som det, og dette er definitivt ikke bra. For å gjøre profilering enklere, vil vi legge det verktøyet som ble nevnt i første trinn. Her er koden for å legge i Main.as:
privat funksjon init (e: Hendelses = null): void {removeEventListener (Event.ADDED_TO_STAGE, init); //Inngangspunkt 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 å brukes
Trinn 5: Definere en Poolable Object
Søknaden vi bygget i trinn 4 var ganske enkel. Det inneholdt bare et enkelt partikkel effekt, men skapte mye trøbbel i minnet. I dette trinnet, vil vi begynne å jobbe på et objekt basseng for å løse det problemet.
Vårt første skritt mot en god løsning er å tenke på hvordan objektene kan samles uten problemer. I et objekt basseng, må vi alltid sørge for at objektet er laget er klar til bruk, og at objektet returneres er helt "isolert" fra resten av programmet (dvs. eier ingen referanser til andre ting). For å tvinge hver samleobjekt for å kunne gjøre det, skal vi lage en grensesnitt Siden våre partikler vil være poolable, må vi gjøre dem implementere IPoolable. I utgangspunktet vi flytte all koden fra sine konstruktører til fornye () -funksjonen, og eliminere eventuelle eksterne referanser til objektet i ødelegge () -funksjonen. Her er hva det skal se slik ut: Twitter /* INTERFACE IPoolable * /public funksjon blir ødelagt (): Boolean {return _destroyed;} offentlig funksjon fornye (): 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; _speed = 150; //Piksler per sekund _lifeTime = 1000; //Miliseconds} offentlig funksjon ødelegge (): void {if (_destroyed) {return; } _destroyed = True; graphics.clear ();} Konstruktøren også skal ikke kreve noen argumenter lenger. Hvis du ønsker å sende noen informasjon til objektet, må du gjøre det gjennom funksjoner nå. På grunn av måten at fornye () -funksjonen fungerer nå, vi må også sette _destroyed å true i konstruktøren slik at funksjonen kan kjøres. Med det, vi har bare tilpasset vår Particle klassen til å oppføre seg som en IPoolable. På den måten vil objektet bassenget kunne lage en pool av partikler Det er på tide nå å skape et fleksibelt objekt basseng som kan pool ethvert objekt vi ønsker. Dette bassenget vil fungere litt som en fabrikk. Stedet for å bruke den nye nøkkelordet for å opprette objekter du kan bruke, vil vi i stedet kalle en metode i bassenget som returnerer til oss et objekt I forbindelse med enkelhet , vil objektet bassenget være en Singleton. På den måten kan vi få tilgang til den hvor som helst innenfor vår kode. Start med å lage en ny klasse kalt "ObjectPool" og legge inn koden for å gjøre det en Singleton: Den variable _allowInstantiation er kjernen i denne Singleton implementering:. Det er privat, så bare egen klasse kan endre, og det eneste stedet hvor det skal endres er før du oppretter den første forekomsten av det Vi må nå bestemme seg for hvordan de skal holde bassengene inne i denne klassen. Siden det vil være global (dvs. kan pool ethvert objekt i programmet), må vi først komme opp med en måte å alltid ha et unikt navn for hvert basseng. Hvordan du gjør det? Det er mange måter, men den beste jeg har funnet så langt er å bruke objektenes egne klasse navn som bassenget navn. På den måten kunne vi ha en "Particle" pool, en "Enemy" pool og så videre ... men det er et annet problem. Klasse navn bare må være unikt innenfor sine pakker, så for eksempel en klasse "BaseObject" innenfor "fiender" pakken og en klasse "BaseObject" innenfor "strukturer" pakken ville 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 for å hjelpe oss. I utgangspunktet er dette funksjonen genererer en streng med full klasse navn, inkludert eventuelle pakker. Så nå kan vi bruke hver objektets kvalifiserte klassenavnet som identifikator for sine respektive bassenger! Dette er hva vi vil legge i neste trinn Nå som vi har en måte å identifisere bassenger, er det på tide å legge inn koden som skaper dem . Vårt objekt bassenget bør være fleksibel nok til å støtte både statiske og dynamiske bassenger (vi snakket om disse i trinn 3, husker du?). Vi må også være i stand til å lagre størrelsen på hver basseng og hvor mange aktive stedene er det i hver enkelt. En god løsning for det er å opprette en privat klasse med all denne informasjonen og lagre alle bassenger innenfor et objekt: Koden ovenfor skaper privat klasse som vil inneholde all informasjon om et basseng. Vi skapte også _pools objekt å holde alle objekt bassenger. Nedenfor vil vi skape den funksjon som registrerer et basseng i klassen: Denne koden ser litt mer komplisert, men ikke få panikk. Alt er forklart her. Den første hvis setningen ser veldig rart. Du kan aldri ha sett disse funksjonene før, så her er hva den gjør: Koden etter denne kontrollen skaper bare en oppføring i _pools hvis man ikke allerede eksisterer. Etter det, kaller PoolInfo konstruktør initialize () funksjon i den klassen, effektivt skaper bassenget med størrelsen vi ønsker. Det er nå klar til å brukes I det siste trinnet var vi i stand til å skape den funksjonen som registrerer et objekt basseng, men nå trenger vi å få et objekt for å kunne bruke den. Det er veldig enkelt: vi får et objekt hvis bassenget er ikke tom, og returnere den. Hvis bassenget er tomt, sjekker vi om det er dynamisk; hvis så, vi øker sin størrelse, og deretter opprette et nytt objekt og returnere den. Hvis ikke, returnerer vi null. (Du kan også velge å kaste en feil, men det er bedre å bare returnere null og gjøre koden arbeidet rundt denne situasjonen når det skjer.) Her er getObj () -funksjonen: I funksjonen, må du først sjekker vi at utvalget faktisk eksisterer. Forutsatt at vilkåret er oppfylt, vi sjekke om bassenget er tom: hvis det er, men det er dynamisk, skaper vi et nytt objekt og legge ut i bassenget. Hvis bassenget ikke er dynamisk, vi stoppe koden der og bare returnere null. Hvis bassenget har fortsatt et objekt, får vi objektet nærmest til begynnelsen av bassenget og kaller fornye () på den. Dette er viktig: grunnen til at vi kaller fornye () på et objekt som allerede var i bassenget er å garantere at dette objektet vil bli gitt på en "brukbar" state Du lurer kanskje på. Hvorfor don ' t du også bruke den kule sjekk med describeType () i denne funksjonen? Vel, svaret er enkelt: describeType () oppretter en XML hver gang vi kaller det, så det er veldig viktig å unngå etablering av objekter som bruker mye minne, og at vi ikke kan kontrollere. Dessuten bare sjekke for å se om bassenget virkelig eksisterer er nok: hvis klassen gikk ikke implementerer IPoolable, betyr at vi ville ikke engang være i stand til å lage et basseng for det. Hvis det ikke er et basseng for det, så vi definitivt ta denne saken i vår hvis setningen i begynnelsen av funksjonen. Vi kan nå endre vår hovedklassen og bruke objektet bassenget! Sjekk det ut: Hit kompilere og profilere minnebruk! Her er hva jeg fikk: Det er litt kult, er det ikke Vi har implementert et objekt basseng som gir oss stedene. Det er utrolig! Men det er ikke over ennå. Vi er fortsatt bare få objekter, men aldri returnere dem når vi ikke trenger dem lenger. Tid for å legge til en funksjon for å gå tilbake gjenstander inne ObjectPool.as: La oss gå gjennom funksjon: den første er å sjekke om det er et basseng for objektet som ble vedtatt. Du er vant til at koden - den eneste forskjellen er at nå bruker vi en gjenstand i stedet for en klasse for å få kvalifiserte navnet, men det endrer ikke output) Deretter får vi. indeksen for varen i bassenget. Hvis det ikke er i bassenget, vi bare ignorere det. Når vi har bekreftet at objektet er i bassenget, må vi bryte bassenget på hvor objektet er for tiden på og sett objektet på slutten av den. Og hvorfor? Fordi vi teller de brukte gjenstander fra begynnelsen av bassenget, må vi omorganisere bassenget for å gjøre alle returnert og ubrukte objekter for å være på slutten av den. Og det er det vi gjør i denne funksjonen. For statiske objekt bassenger, skaper vi en vektor objekt som har fast lengde. På grunn av det, kan vi ikke spleise () det og push () objekter tilbake. Løsningen på dette er å endre den faste eiendommen av disse vektorer som usann, fjerne objektet og legge den tilbake på slutten, og deretter endre eiendommen tilbake til sann. Vi trenger også å redusere antall aktive stedene. Etter det er vi ferdige retur objektet. Nå som vi har skapt koden for å returnere et objekt, kan vi gjøre våre partikler returnere seg til bassenget når de når slutten av sin levetid. Inne Particle.as: Legg merke til at vi har lagt et kall til ObjectPool.instance.returnObj () der inne. Det er det som gjør objektet tilbake selv til bassenget. Vi kan nå teste og profilere vår app: Og der vi går! Stabil minnet selv når hundrevis av klikk ble gjort! Du vet nå hvordan du oppretter og bruker et objekt bassenget for å holde app minnebruk stabilt. Klassen vi bygget bygget kan brukes hvor som helst, og det er veldig enkelt å tilpasse koden til det: i begynnelsen av programmet ditt, opprette objekt bassenger for alle slags objekt du vil bassenget, og når det er et nytt søkeord (som betyr opprettelsen av en forekomst), erstatte den med en oppfordring til den funksjonen som blir et objekt for deg. Ikke glem å implementere metoder som grensesnittet IPoolable krever! Å holde minnebruk stabilt er veldig viktig. Det sparer deg for mye trøbbel senere i prosjektet når alt begynner å falle fra hverandre med unrecycled tilfeller fortsatt reagere på hendelsen lyttere, gjenstander fylle opp minnet du har tilgjengelig til å bruke og med søppeltømmeren løping og bremse ned alt. En god anbefaling er å alltid bruke objekt pooling fra nå av, og du vil merke ditt liv vil bli mye enklere Legg også merke til at selv om denne opplæringen var rettet for Flash, konsepter utviklet i den er global.: du kan bruke den på AIR apps, mobile apps og hvor som helst det passer. Takk for lesing!
. Dette grensesnittet vil definere to viktige funksjoner som objektet må ha: fornye () og ødelegge (). På den måten kan vi alltid ringe disse metodene uten å bekymre deg om hvorvidt objektet har dem (fordi det vil ha). Dette betyr også at hvert objekt vi ønsker å samle trenger for å implementere dette grensesnittet. Så her er det:
pakke {public interface IPoolable {funksjon blir ødelagt (): Boolean; funksjon fornye (): void; funksjon ødelegge (): void; }}
Trinn 6:. Starte Object Pool
pakken {public class ObjectPool {private static Var _instance: ObjectPool; privat Static Var _allowInstantiation: Boolean; public static funksjon få forekomst (): (! _instance) ObjectPool {if {_allowInstantiation = true; _instance = new ObjectPool (); _allowInstantiation = false; } Returnere _instance; } Offentlig funksjon ObjectPool () {if (_allowInstantiation!) {Kaste nytt Feil ("Prøver å bruke et Singleton!"); }}}}
Trinn 7:. Opprette Pools
pakken {public class ObjectPool {private static Var _instance: ObjectPool; privat Static Var _allowInstantiation: Boolean; private Var _pools: Object; public static funksjon få forekomst (): (! _instance) ObjectPool {if {_allowInstantiation = true; _instance = new ObjectPool (); _allowInstantiation = false; } Returnere _instance; } Offentlig funksjon ObjectPool () {if (_allowInstantiation!) {Kaste nytt Feil ("Prøver å bruke et Singleton!"); } _pools = {}; }}} Class PoolInfo {offentlig Var elementer. Vector < IPoolable >; offentlig Var itemClass: Klasse; offentlig Var størrelse: UINT; offentlig Var aktiv: UINT; offentlig Var isDynamic: Boolean; offentlig funksjon PoolInfo (itemClass: Klasse, størrelse: uint, isDynamic: Boolean = true) {this.itemClass = itemClass; elementer = new Vector. < IPoolable > (størrelse, isDynamic!); this.size = størrelse; this.isDynamic = isDynamic; aktive = 0; initial (); } Private funksjon initialize (): void {for (var i: int = 0; i < størrelse; i ++) {elementer [i] = new itemClass (); }}}
offentlig funksjon registerPool (object: Klasse, størrelse: uint = 1, isDynamic: Boolean = true): void {if ((describeType (object) .factory .implementsInterface (@ typen == "IPoolable") length () >.. 0)) {kaste nytt Feil ("Kan ikke bassenget noe som ikke implementerer IPoolable!"); komme tilbake; } Var qualifiedName: String = getQualifiedClassName (object); hvis {_pools [qualifiedName] = new PoolInfo (object, størrelse, isDynamic) (_pools [qualifiedName]!); }}
describeType () -funksjonen oppretter en XML som inneholder all informasjon om objektet vi passert det
In. Ved en klasse, er alt om det finnes fabrikken lappen.
Inside som beskriver XML alle grensesnittene at klassen implementerer med implementsInterface lappen.
Vi gjør en rask søk for å se om IPoolable grensesnittet er blant dem. Hvis ja, så vet vi at vi kan legge til at klassen til bassenget, fordi vi vil være i stand til å lykkes kaste det som en IObject.
Trinn 8: Å få et objekt
offentlig funksjon getObj ( object: Klasse): IPoolable {var qualifiedName: String = getQualifiedClassName (object); if (! _pools [qualifiedName]) {kaste nytt Feil ("Kan ikke hente et objekt fra et basseng som ikke har blitt registrert!"); komme tilbake; } Var returnObj: IPoolable; if (PoolInfo (_pools [qualifiedName]) aktiv == PoolInfo (_pools [qualifiedName]) størrelse..) {if (PoolInfo (_pools [qualifiedName]) isDynamic.) {returnObj = new object (); PoolInfo (_pools [qualifiedName]) størrelse ++.; PoolInfo (_pools [qualifiedName]) items.push (returnObj.); } Else {return null; }} Else {returnObj = PoolInfo (_pools [qualifiedName]) varer [PoolInfo (_pools [qualifiedName]) aktiv.].; returnObj.renew (); } PoolInfo (_pools [qualifiedName]) aktiv ++.; returnere returnObj;}
privat funksjon init (e: Hendelses = null): void {removeEventListener (Event.ADDED_TO_STAGE, init); //Inngangspunkt stage.addEventListener (MouseEvent.CLICK, createParticles); addEventListener (Event.ENTER_FRAME, updateParticles); _oldTime = getTimer (); ObjectPool.instance.registerPool (Particle, 200, true);} private funksjons createParticles (e: MouseEvent): void {var tempParticle: Partikkel; for (var i: int = 0; i < 10; i ++) {tempParticle = ObjectPool.instance.getObj (Particle) som Particle; tempParticle.x = e.stageX; tempParticle.y = e.stageY; addChild (tempParticle); }}
Trinn 9: Retur Objekter til Pool
offentlig funksjon returnObj (obj: IPoolable): void {var qualifiedName: String = getQualifiedClassName (obj); if (! _pools [qualifiedName]) {kaste nytt Feil ("Kan ikke returnere et objekt fra et basseng som ikke har blitt registrert!"); komme tilbake; } Var objIndex: int = PoolInfo (_pools [qualifiedName]) items.indexOf (obj);. if (objIndex > = 0) {if {PoolInfo (_pools [qualifiedName]) items.fixed = false (PoolInfo (_pools [qualifiedName]) isDynamic!..); } PoolInfo (_pools [qualifiedName]) items.splice (objIndex, 1).; obj.destroy (); PoolInfo (_pools [qualifiedName]) items.push (obj.); hvis {PoolInfo (_pools [qualifiedName]) items.fixed = sant (PoolInfo (_pools [qualifiedName]) isDynamic!..); } PoolInfo (_pools [qualifiedName]) active--.; }}
offentlig funksjon oppdateringen (timePassed: uint): void {//Making partikkelen farten x + = Math.cos (_angle) * _speed * timePassed /1000; y + = tak i Math.sin (_angle) * _speed * timePassed /1000; //Liten lettelser for å gjøre bevegelsen ser ganske _speed - = 120 * timePassed /1000; //Å ta vare på livet og fjerning _lifeTime - = timePassed; if (_lifeTime < = 0) {parent.removeChild (denne); ObjectPool.instance.returnObj (denne); }}
Konklusjon