Minnesanvändning är en aspekt av utveckling som du verkligen måste vara försiktig med, eller det kan sluta sakta ner din app, ta upp mycket minne eller till och med krascha allt. Denna handledning hjälper dig att undvika de dåliga potentiella resultaten!
Låt oss ta en titt på det slutliga resultatet vi ska arbeta för:
Klicka någonstans på scenen för att skapa en fyrverkeringseffekt och hålla koll på minnesprofilen längst upp till vänster.
Om du någonsin har profilerat din ansökan med något profileringsverktyg eller använt någon kod eller ett bibliotek som berättar om den aktuella minnesanvändningen av din ansökan, kanske du har märkt att många gånger minskar minnesanvändningen och går ner igen (om du har tillflyktsort 't, din kod är utmärkt!). Tja, även om dessa spikar som orsakas av stor minnesanvändning ser ganska cool ut, är det inte bra nyheter för antingen din ansökan eller (följaktligen) dina användare. Fortsätt läsa för att förstå varför detta händer och hur man undviker det.
Bilden nedan är ett riktigt bra exempel på dålig minneshantering. Det är från en prototyp av ett spel. Du måste märka två viktiga saker: de stora piggarna på minnesanvändning och minnesanvändningen topp. Toppen är nästan vid 540Mb! Det betyder att den här prototypen ensam nådde nivån att använda 540Mb av användarens dator RAM - och det är något du definitivt vill undvika.
Det här problemet börjar när du börjar skapa många objekt instanser i din ansökan. Oanvända instanser kommer att fortsätta att använda din applikations minne tills skräpuppsamlaren körs, när de slås ut - vilket orsakar de stora spikarna. En ännu sämre situation händer när instanserna helt enkelt inte kommer att fördelas, vilket gör att programmets minnesanvändning fortsätter att växa tills någonting kraschar eller bryter. Om du vill veta mer om det senare problemet och hur man undviker det, läs den här snabba tipsen om skräpsamling.
I denna handledning tar vi inte upp några problem med soporuppsamlare. Vi arbetar istället med att bygga strukturer som effektivt håller föremål i minnet, vilket gör användningen helt stabil och därigenom håller sopkollektor från att städa upp minnet, vilket gör ansökan snabbare. Ta en titt på minnesanvändningen av samma prototyp ovan, men den här tiden optimeras med de tekniker som visas här:
All denna förbättring kan uppnås genom att använda objektpooling. Läs vidare för att förstå vad det är och hur det fungerar.
Objektpooling är en teknik där ett fördefinierat antal objekt skapas när applikationen initieras och hålls i minnet under hela applikationslivet. Objektpoolen ger objekt när ansökan begär dem och återställer objekten till startläget när programmet är färdigt med hjälp av dem. Det finns många typer av objektpooler, men vi tar bara en titt på två av dem: det statiska och det dynamiska objektet poolar.
Den statiska objektpoolen skapar ett definierat antal objekt och håller bara den mängd objekt under hela programmets livstid. Om ett objekt begärs, men poolen har redan givit alla sina föremål, återgår poolen null. När du använder denna typ av pool är det nödvändigt att ta itu med problem som att begära ett objekt och inte få tillbaka något.
Den dynamiska objektpoolen skapar också ett definierat antal objekt vid initialiseringen, men när ett objekt begärs och poolen är tom skapar poolen en annan instans automatiskt och returnerar det objektet, ökar poolstorleken och lägger till det nya objektet.
I denna handledning bygger vi en enkel applikation som genererar partiklar när användaren klickar på skärmen. Dessa partiklar kommer att ha en begränsad livstid och kommer sedan att tas bort från skärmen och återvända till poolen. För att göra det kommer vi först att skapa denna applikation utan objektbassning och kontrollera minnesanvändningen och sedan implementera objektpoolen och jämföra minnesanvändningen till tidigare.
Öppna FlashDevelop (se den här guiden) och skapa ett nytt AS3-projekt. Vi kommer att använda en enkel liten färgad torg som partikelbilden, som kommer att ritas med kod och flyttas enligt en slumpvinkel. Skapa en ny klass som heter Particle som utökar Sprite. Jag antar att du kan hantera skapandet av en partikel och bara markera de aspekter som håller reda på partikelns livstid och borttagning från skärmen. Du kan ta tag i hela källkoden för denna handledning högst upp på sidan om du har problem med att skapa partikeln.
privat var _lifeTime: int; public function update (timePassed: uint): void // Gör partikelrörelsen x + = Math.cos (_angle) * _speed * timePassed / 1000; y + = Math.sin (_angle) * _speed * timePassed / 1000; // Liten lättnad att göra rörelsen ser fin ut _speed - = 120 * timePassed / 1000; // Ta hand om livstid och borttagning _lifeTime - = timePassed; om (_lifeTime <= 0) parent.removeChild(this);
Koden ovan är koden ansvarig för partikelns borttagning från skärmen. Vi skapar en variabel som heter _livstid
att innehålla antalet milisekunder att partikeln kommer att finnas på skärmen. Vi initierar som standard sitt värde till 1000 på konstruktören. De uppdatering()
funktion kallas varje ram och tar emot mängden milisekunder som passerade mellan ramar, så att det kan minska partikelns livslängd. När detta värde når 0 eller mindre frågar partikeln automatiskt sin förälder att ta bort den från skärmen. Resten av koden tar hand om partikelns rörelse.
Nu ska vi göra en massa av dessa skapas när ett musklick upptäcks. Gå till Main.as:
privat var _oldTime: uint; privat var _lapsad: uint; privat funktion 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 (); privatfunktionsuppdateringPartiklar (e: Event): void _elapsed = getTimer () - _oldTime; _oldTime + = _elapsed; för (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));
Koden för uppdatering av partiklarna bör vara bekant för dig: det är rötterna till en enkel tidsbaserad slinga, som vanligtvis används i spel. Glöm inte importansökningarna:
importera flash.events.Event; importera flash.events.MouseEvent; importera flash.utils.getTimer;
Du kan nu testa din ansökan och profilera den med hjälp av FlashDevelops inbyggda profiler. Klicka på en massa gånger på skärmen. Så här ser min minnesanvändning ut:
Jag klickade tills sopsamlare började springa. Ansökan skapades över 2000 partiklar som samlades in. Ser det ut som minnesanvändningen av den prototypen? Det ser ut som det, och det är definitivt inte bra. För att göra profilering enklare lägger vi till verktyget som nämndes i det första steget. Här är koden som ska läggas till i Main.as:
privat funktion init (e: Event = null): void removeEventListener (Event.ADDED_TO_STAGE, init); // entry point stage.addEventListener (MouseEvent.CLICK, createParticles); addEventListener (Event.ENTER_FRAME, updateParticles); addChild (nya statistik ()); _oldTime = getTimer ();
Glöm inte att importera net.hires.debug.Stats
och det är redo att användas!
Programmet vi byggde i steg 4 var ganska enkelt. Den innehöll bara en enkel partikeleffekt, men skapade mycket problem i minnet. I det här steget börjar vi arbeta med en objektpool för att lösa det problemet.
Vårt första steg mot en bra lösning är att tänka på hur objekten kan slås samman utan problem. I en objektpool måste vi alltid se till att det skapade objektet är färdigt att använda och att objektet som returneras är helt "isolerat" från resten av programmet (det vill säga innehåller inga referenser till andra saker). För att tvinga varje poolat objekt att kunna göra det ska vi skapa en gränssnitt. Detta gränssnitt definierar två viktiga funktioner som objektet måste ha: förnya()
och förstöra()
. På det sättet kan vi alltid ringa dessa metoder utan att oroa oss för huruvida objektet har dem (eftersom det kommer att ha) eller inte. Detta innebär också att varje objekt vi vill poola måste implementera detta gränssnitt. Så här är det:
paket public interface IPoolable function get destroyed (): Boolean; funktionen förnya (): void; funktion förstöra (): void;
Eftersom våra partiklar kommer att vara poolbara måste vi få dem att genomföras IPoolable
. I grund och botten flyttar vi all kod från sina konstruktörer till förnya()
funktionen och eliminera eventuella externa referenser till objektet i förstöra()
fungera. Så här ska det se ut:
/ * INTERFACE IPoolable * / public function bli förstörd (): Boolean return _destroyed; public function renew (): void if (! _destroyed) return; _destroyed = false; Graph.beginFill (uint (Math.random () * 0xFFFFFF), 0,5 + (Math.random () * 0,5)); Graph.drawRect (-1.5, -1.5, 3, 3); graphics.endFill (); _angle = Math.random () * Math.PI * 2; _speed = 150; // Pixlar per sekund _lifeTime = 1000; // Miliseconds public function destroy (): void if (_destroyed) return; _destroyed = true; graphics.clear ();
Konstruktören bör inte längre behöva några argument. Om du vill skicka information till objektet måste du göra det genom funktioner nu. På grund av det sätt som förnya()
funktionen fungerar nu, vi måste också ställa in _förstörd
till Sann
i konstruktorn så att funktionen kan köras.
Med det har vi justerat vårt Partikel
klass att uppträda som en IPoolable
. På så sätt kan objektpoolen skapa en pool av partiklar.
Det är dags att skapa en flexibel objektpool som kan poola alla objekt vi vill ha. Denna pool fungerar lite som en fabrik: istället för att använda ny
sökord för att skapa objekt som du kan använda, kommer vi istället att ringa en metod i poolen som returnerar ett objekt till oss.
För enkelhetens ändamål kommer objektpoolen att vara en Singleton. På så sätt kan vi komma åt det var som helst inom vår kod. Börja med att skapa en ny klass som heter "ObjectPool" och lägga till koden för att göra det till en Singleton:
paket public class ObjectPool private static var _instance: ObjectPool; privat statisk var _allowInstantiation: Boolean; offentlig statisk funktion få instans (): ObjectPool if (! _instance) _allowInstantiation = true; _instance = nytt ObjectPool (); _allowInstantiation = false; returnera instans offentliga funktionen ObjectPool () if (! _allowInstantiation) kasta nytt fel ("Försöker inställa en Singleton!");
Variabeln _allowInstantiation
är kärnan i det här Singleton-genomförandet: det är privat, så endast den egna klassen kan modifiera, och den enda platsen där den ska ändras är innan den skapas första gången av den.
Vi måste nu bestämma hur vi håller poolerna inne i den här klassen. Eftersom det kommer att bli globalt (det vill säga kan poola något objekt i din ansökan) måste vi först få ett sätt att alltid ha ett unikt namn för varje pool. Hur gör man det? Det finns många sätt, men det bästa jag hittat hittills är att använda objektens egna klassnamn som poolnamn. På det sättet kunde vi ha en "Particle" -pool, en "Enemy" -pool och så vidare ... men det finns ett annat problem. Klassnamn måste bara vara unika inom sina paket, så till exempel kan en klass "BaseObject" i "fiender" -paketet och en klass "BaseObject" inom "strukturer" -paketet tillåtas. Det skulle orsaka problem i poolen.
Tanken att använda klassnamn som identifierare för poolerna är fortfarande stor, och det är här flash.utils.getQualifiedClassName ()
kommer att hjälpa oss. I grunden genererar denna funktion en sträng med hela klassnamnet, inklusive eventuella paket. Så nu kan vi använda varje objekts kvalificerade klassnamn som identifierare för sina respektive pooler! Detta ska vi lägga till i nästa steg.
Nu när vi har ett sätt att identifiera pooler är det dags att lägga till koden som skapar dem. Vår objektpool bör vara tillräckligt flexibel för att stödja både statiska och dynamiska pooler (vi pratade om dessa i steg 3, kom ihåg?). Vi måste också kunna lagra storleken på varje pool och hur många aktiva objekt det finns i var och en. En bra lösning för det är att skapa en privat klass med all denna information och lagra alla pooler inom en Objekt
:
paket public class ObjectPool private static var _instance: ObjectPool; privat statisk var _allowInstantiation: Boolean; privata var _pools: Objekt; offentlig statisk funktion få instans (): ObjectPool if (! _instance) _allowInstantiation = true; _instance = nytt ObjectPool (); _allowInstantiation = false; returnera instans offentliga funktionen ObjectPool () if (! _allowInstantiation) kasta nytt fel ("Försöker inställa en Singleton!"); _pools = ; klass PoolInfo public var items: Vector.; offentlig var itemClass: Class; allmän var storlek: uint; allmänheten var aktiv: uint; allmänhet är isynamisk: booleska; allmän funktion PoolInfo (itemClass: Klass, storlek: uint, isDynamic: Boolean = true) this.itemClass = itemClass; objekt = ny vektor. (storlek,! isDynamisk); this.size = size; this.isDynamic = isDynamic; aktiv = 0; initialisera (); privat funktion initiera (): void for (var i: int = 0; i < size; i++) items[i] = new itemClass();
Koden ovan skapar den privata klassen som innehåller all information om en pool. Vi skapade också _pools
objekt att hålla alla objektpooler. Nedan skapar vi den funktion som registrerar en pool i klassen:
public function registerPool (objectClass: Class, size: uint = 1, isDynamic: Boolean = true): void if (! (describeType (objectClass) .factory.implementsInterface. (@ type == "IPoolable" 0)) kasta nytt fel ("Kan inte slå ihop något som inte implementerar IPoolable!"); lämna tillbaka; var kvalificeradName: String = getQualifiedClassName (objectClass); om (! _pools [qualifiedName]) _pools [qualifiedName] = ny PoolInfo (objectClass, size, isDynamic);
Den här koden ser lite hårdare ut, men inte panik. Det är allt förklarat här. Den första om
uttalandet ser väldigt konstigt ut. Du har kanske aldrig sett dessa funktioner innan, så här är vad det gör:
fabrik
märka.implementsInterface
märka.IPoolable
gränssnittet är bland dem. Om så är fallet vet vi att vi kan lägga till den klassen till poolen, för att vi kommer att kunna framgångsrikt kasta den som en Jag protesterar
.Koden efter denna kontroll skapar bara en post inom _pools
om man inte redan existerade. Därefter PoolInfo
konstruktören kallar initialisera ()
fungera inom den klassen, effektivt skapa poolen med den storlek vi vill ha. Det är nu klart att användas!
I det sista steget kunde vi skapa den funktion som registrerar en objektpool, men nu måste vi få ett objekt för att kunna använda det. Det är mycket enkelt: vi får ett föremål om poolen inte är tom och returnerar den. Om poolen är tom kontrollerar vi om den är dynamisk; Om så är fallet ökar vi dess storlek och skapar sedan ett nytt objekt och returnerar det. Om inte, returnerar vi null. (Du kan också välja att kasta ett fel, men det är bättre att bara returnera null och få din kod att fungera runt den här situationen när det händer.)
Här är getObj ()
fungera:
offentlig funktion getObj (objectClass: Class): IPoolable var kvalificeradName: String = getQualifiedClassName (objectClass); om ! _pools [qualifiedName]) kasta nytt fel ("Kan inte få ett objekt från en pool som inte har registrerats!"); lämna tillbaka; var returnObj: IPoolable; if (PoolInfo (_pools [qualifiedName]). aktiv == PoolInfo (_pools [qualifiedName]) .storlek) if (PoolInfo (_pools [qualifiedName]) .Dynamic) returnObj = new objectClass (); PoolInfo (_pools [qualifiedName]) storlek ++.; PoolInfo (_pools [qualifiedName]) items.push (returnObj).; annars return null; else returnObj = PoolInfo (_pools [qualifiedName]). objekt [PoolInfo (_pools [qualifiedName]). aktiv]; returnObj.renew (); PoolInfo (_pools [qualifiedName]). Aktiv ++; återvända returObj;
I funktionen kontrollerar vi först att poolen faktiskt existerar. Om vi antar att villkoret är uppfyllt, kontrollerar vi om poolen är tom: om det är men det är dynamiskt, skapar vi ett nytt objekt och lägger till i poolen. Om poolen inte är dynamisk stoppar vi koden där och returnerar bara null. Om poolen fortfarande har ett objekt får vi objektet närmast poolens början och samtalet förnya()
på det. Detta är viktigt: anledningen till att vi ringer förnya()
på ett objekt som redan var i poolen är att garantera att detta objekt kommer att ges i ett "användbart" tillstånd.
Du undrar förmodligen: varför använder du inte den svala kontrollen med describeType ()
i den här funktionen? Jo, svaret är enkelt: describeType ()
skapar en XML varje Den tid vi kallar det, så är det väldigt viktigt att undvika skapandet av objekt som använder mycket minne och att vi inte kan styra. Förutom att bara kontrollera om poolen verkligen existerar är det tillräckligt: om klassen passerade inte implementerar IPoolable
, det betyder att vi inte ens skulle kunna skapa en pool för den. Om det inte finns en pool för det, tar vi definitivt fallet i vårt om
uttalande i början av funktionen.
Vi kan nu ändra vår Huvudsaklig
klass och använd objektpoolen! Kolla in det:
privat funktion 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 funktion createParticles (e: MouseEvent): void var tempParticle: Particle; för (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 kompilera och profil minnesanvändningen! Här är vad jag fick:
Det är ganska coolt, är det inte?
Vi har framgångsrikt implementerat en objektpool som ger oss föremål. Det är fantastiskt! Men det är inte över än. Vi får fortfarande objekt, men återvänder aldrig när vi inte behöver dem längre. Tid att lägga till en funktion för att returnera objekt inuti ObjectPool.as
:
allmän funktion returnObj (obj: IPoolable): void var kvalificeradName: String = getQualifiedClassName (obj); om ! _pools [qualifiedName]) kasta nytt fel ("kan inte returnera ett objekt från en pool som inte har registrerats!"); lämna tillbaka; var objIndex: int = PoolInfo (_pools [qualifiedName]). items.indexOf (obj); om (objIndex> = 0) if (! PoolInfo (_pools [qualifiedName]) .Dynamic) PoolInfo (_pools [qualifiedName]). items.fixed = false; PoolInfo (_pools [qualifiedName]). Items.splice (objIndex, 1); obj.destroy (); PoolInfo (_pools [qualifiedName]) items.push (obj).; om (! PoolInfo (_pools [qualifiedName]) .Dynamic) PoolInfo (_pools [qualifiedName]). items.fixed = true; PoolInfo (_pools [qualifiedName]). Active--;
Låt oss gå igenom funktionen: Det första är att kontrollera om det finns en pool av objektet som passerade. Du är van vid den koden - den enda skillnaden är att vi nu använder ett objekt istället för en klass för att få det kvalificerade namnet, men det ändrar inte utmatningen).
Därefter får vi index över objektet i poolen. Om det inte finns i poolen, ignorerar vi bara det. När vi väl har kontrollerat att objektet är i poolen måste vi bryta poolen där objektet är för närvarande och återinsätta objektet i slutet av det. Och varför? Eftersom vi räknar de använda objekten från poolens början måste vi omorganisera poolen så att alla återvände och oanvända objekt kan vara i slutet av det. Och det är vad vi gör i den här funktionen.
För statiska objektpooler skapar vi en Vektor
objekt som har fast längd. På grund av det kan vi inte splitsa()
det och tryck()
objekt tillbaka. Lösningen med detta är att ändra fast
egenskapen hos dem Vektor
s till falsk
, ta bort objektet och lägg till det i slutet och ändra sedan egenskapen till Sann
. Vi måste också minska antalet aktiva objekt. Därefter är vi färdiga att återställa objektet.
Nu när vi har skapat koden för att returnera ett objekt, kan vi få våra partiklar att återvända till poolen när de når slutet av deras livstider. Inuti Particle.as
:
public function update (timePassed: uint): void // Gör partikelrörelsen x + = Math.cos (_angle) * _speed * timePassed / 1000; y + = Math.sin (_angle) * _speed * timePassed / 1000; // Liten lättnad att göra rörelsen ser fin ut _speed - = 120 * timePassed / 1000; // Ta hand om livstid och borttagning _lifeTime - = timePassed; om (_lifeTime <= 0) parent.removeChild(this); ObjectPool.instance.returnObj(this);
Observera att vi lagt till ett samtal till ObjectPool.instance.returnObj ()
där inne. Det är det som gör att objektet återvänder till poolen. Vi kan nu testa och profilera vår app:
Och där går vi! Stabilt minne även när hundratals klick gjordes!
Nu vet du hur du skapar och använder en objektpool för att hålla din apps minnesanvändning stabil. Den klass vi byggt byggde kan användas var som helst och det är väldigt enkelt att anpassa din kod till den: i början av din app skapar du objektpooler för varje typ av objekt du vill poola, och när det finns en ny
nyckelord (vilket innebär att en instans skapas), ersätt den med ett samtal till den funktion som får ett objekt för dig. Glöm inte att implementera metoderna som gränssnittet IPoolable
kräver!
Att hålla din minnesanvändning stabil är verkligen viktigt. Det sparar dig mycket problem senare i ditt projekt när allt börjar falla ihop med orecyklerade instanser som fortfarande svarar på händelsehörare, objekt fyller upp minnet du har tillgång till och med sopsamlaren körs och saktar ner allt. En bra rekommendation är att alltid använda objektpooling från och med nu, och du kommer märka att ditt liv blir mycket enklare.
Observera också att även om denna handledning riktades mot Flash, är de begrepp som utvecklats globalt: du kan använda den på AIR-appar, mobilappar och var som helst den passar. Tack för att du läser!