L'utilizzo della memoria è un aspetto dello sviluppo su cui devi stare molto attento, altrimenti potrebbe rallentare la tua app, occupare molta memoria o persino bloccare tutto. Questo tutorial ti aiuterà ad evitare quei cattivi risultati potenziali!
Diamo un'occhiata al risultato finale su cui lavoreremo:
Fai clic in qualsiasi punto del palco per creare un effetto pirotecnico e tieni d'occhio il profiler di memoria nell'angolo in alto a sinistra.
Se hai mai profilato la tua applicazione utilizzando uno strumento di profilazione o hai utilizzato un codice o una libreria che ti indichi l'attuale utilizzo della memoria della tua applicazione, potresti aver notato che molte volte l'utilizzo della memoria sale e poi scende di nuovo (se non hai il tuo codice è superbo!). Bene, anche se questi picchi causati da un grande utilizzo della memoria sembrano piuttosto interessanti, non sono buone notizie né per l'applicazione né per gli utenti. Continua a leggere per capire perché questo accade e come evitarlo.
L'immagine sotto è un ottimo esempio di scarsa gestione della memoria. È da un prototipo di un gioco. È necessario notare due cose importanti: i grandi picchi di utilizzo della memoria e il picco dell'utilizzo della memoria. Il picco è quasi a 540 Mb! Ciò significa che questo prototipo da solo ha raggiunto il punto di utilizzo di 540 Mb di RAM del computer dell'utente, e questo è qualcosa che si vuole assolutamente evitare.
Questo problema inizia quando inizi a creare molte istanze di oggetti nell'applicazione. Le istanze non utilizzate continueranno a utilizzare la memoria dell'applicazione fino a quando il garbage collector non viene eseguito, quando vengono deallocati, causando i picchi. Una situazione ancora peggiore si verifica quando le istanze semplicemente non vengono deallocate, facendo sì che l'utilizzo della memoria della propria applicazione continui a crescere fino a quando qualcosa non va in crash o si rompe. Se vuoi saperne di più su quest'ultimo problema e su come evitarlo, leggi questo suggerimento rapido sulla garbage collection.
In questo tutorial non affronteremo alcun problema di garbage collector. Lavoreremo invece sulla costruzione di strutture che mantengano in modo efficiente gli oggetti nella memoria, rendendo il loro utilizzo completamente stabile e impedendo al garbage collector di ripulire la memoria, rendendo l'applicazione più veloce. Dai un'occhiata all'utilizzo della memoria dello stesso prototipo sopra, ma questa volta ottimizzato con le tecniche mostrate qui:
Tutto questo miglioramento può essere ottenuto utilizzando il pool di oggetti. Continua a leggere per capire di cosa si tratta e come funziona.
Il pooling degli oggetti è una tecnica in cui un numero predefinito di oggetti viene creato quando l'applicazione viene inizializzata e mantenuta in memoria durante l'intera durata dell'applicazione. Il pool di oggetti fornisce oggetti quando l'applicazione li richiede e reimposta gli oggetti allo stato iniziale quando l'applicazione ha finito di usarli. Esistono molti tipi di pool di oggetti, ma daremo solo un'occhiata a due di essi: i pool di oggetti statici e dinamici.
Il pool di oggetti statici crea un numero definito di oggetti e mantiene solo la quantità di oggetti durante l'intera durata dell'applicazione. Se viene richiesto un oggetto ma il pool ha già fornito tutti gli oggetti, il pool restituisce null. Quando si utilizza questo tipo di pool, è necessario risolvere problemi come richiedere un oggetto e non ottenere nulla indietro.
Il pool di oggetti dinamico crea anche un numero definito di oggetti durante l'inizializzazione, ma quando viene richiesto un oggetto e il pool è vuoto, il pool crea automaticamente un'altra istanza e restituisce quell'oggetto, aumentando la dimensione del pool e aggiungendo il nuovo oggetto ad esso.
In questo tutorial creeremo una semplice applicazione che genera particelle quando l'utente fa clic sullo schermo. Queste particelle avranno una durata limitata, quindi verranno rimosse dallo schermo e restituite alla piscina. Per fare ciò, creeremo prima questa applicazione senza il pool di oggetti e controlleremo l'utilizzo della memoria, quindi implementeremo il pool di oggetti e confronteremo l'utilizzo della memoria prima.
Apri FlashDevelop (guarda questa guida) e crea un nuovo progetto AS3. Useremo un semplice quadrato colorato come l'immagine della particella, che verrà disegnata con il codice e si muoverà secondo un angolo casuale. Crea una nuova classe chiamata Particle che estende Sprite. Immagino che tu possa gestire la creazione di una particella e solo evidenziare gli aspetti che terranno traccia della vita della particella e della rimozione dallo schermo. Puoi prendere il codice sorgente completo di questo tutorial nella parte superiore della pagina se hai problemi a creare la particella.
private var _lifeTime: int; aggiornamento della funzione pubblica (timePassed: uint): void // Esecuzione della mossa delle particelle x + = Math.cos (_angle) * _speed * timePassed / 1000; y + = Math.sin (_angle) * _speed * timePassed / 1000; // Small easing per far sembrare il movimento piuttosto _speed - = 120 * timePassed / 1000; // Prendersi cura della durata e della rimozione _lifeTime - = timePassed; se (_lifeTime <= 0) parent.removeChild(this);
Il codice sopra è il codice responsabile della rimozione della particella dallo schermo. Creiamo una variabile chiamata _tutta la vita
per contenere il numero di millisecondi che la particella sarà sullo schermo. Inizializziamo per impostazione predefinita il suo valore a 1000 sul costruttore. Il aggiornare()
la funzione viene chiamata ogni frame e riceve la quantità di millisecondi che passano tra i frame, in modo che possa diminuire il valore della durata della particella. Quando questo valore raggiunge 0 o meno, la particella chiede automaticamente al genitore di rimuoverlo dallo schermo. Il resto del codice si occupa del movimento della particella.
Ora ne creeremo una serie quando viene rilevato un clic del mouse. Vai a Main.as:
private var _oldTime: uint; private var _lapsed: uint; funzione privata 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 (); private function updateParticles (e: Event): void _apsaps = getTimer () - _oldTime; _oldTime + = _elapsed; per (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));
Il codice per l'aggiornamento delle particelle dovrebbe esserti familiare: è la radice di un semplice loop basato sul tempo, comunemente usato nei giochi. Non dimenticare le dichiarazioni di importazione:
import flash.events.Event; import flash.events.MouseEvent; import flash.utils.getTimer;
Ora puoi testare la tua applicazione e il profilo usando il profiler incorporato di FlashDevelop. Fare clic su un mucchio di volte sullo schermo. Ecco come appariva la mia memoria:
Ho fatto clic finché il Garbage Collector non ha iniziato a funzionare. L'applicazione ha creato oltre 2000 particelle che sono state raccolte. Comincia a sembrare l'utilizzo della memoria di quel prototipo? Sembra, e questo non è sicuramente buono. Per semplificare la profilazione, aggiungeremo l'utilità menzionata nel primo passaggio. Ecco il codice da aggiungere in Main.as:
funzione privata init (e: Event = null): void removeEventListener (Event.ADDED_TO_STAGE, init); // entry point stage.addEventListener (MouseEvent.CLICK, createParticles); addEventListener (Event.ENTER_FRAME, updateParticles); addChild (new Stats ()); _oldTime = getTimer ();
Non dimenticare di importare net.hires.debug.Stats
ed è pronto per essere usato!
L'applicazione che abbiamo creato nel passaggio 4 è stata piuttosto semplice. Aveva solo un semplice effetto particellare, ma creava molti problemi nella memoria. In questo passaggio, inizieremo a lavorare su un pool di oggetti per risolvere il problema.
Il nostro primo passo verso una buona soluzione è pensare a come gli oggetti possono essere raggruppati senza problemi. In un pool di oggetti, è necessario accertarsi sempre che l'oggetto creato sia pronto per l'uso e che l'oggetto restituito sia completamente "isolato" dal resto dell'applicazione (ovvero non contiene riferimenti ad altre cose). Per forzare ogni oggetto in pool a farlo, creeremo un interfaccia. Questa interfaccia definirà due funzioni importanti che l'oggetto deve avere: rinnovare()
e distruggere()
. In questo modo, possiamo sempre chiamare questi metodi senza preoccuparci se l'oggetto li ha o meno (perché avrà). Ciò significa anche che ogni oggetto che vogliamo raggruppare dovrà implementare questa interfaccia. Quindi eccolo qui:
package interfaccia pubblica IPoolable function get destroyed (): Boolean; function renew (): void; function destroy (): void;
Dal momento che le nostre particelle saranno aggregabili, dobbiamo farle implementare IPoolable
. Fondamentalmente spostiamo tutto il codice dai loro costruttori al rinnovare()
funzione ed elimina qualsiasi riferimento esterno all'oggetto nel distruggere()
funzione. Ecco come dovrebbe essere:
/ * 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; _speed = 150; // Pixel al secondo _lifeTime = 1000; // Miliseconds public function destroy (): void if (_destroyed) return; _destroyed = true; graphics.clear ();
Inoltre, il costruttore non dovrebbe richiedere ulteriori argomenti. Se vuoi passare qualsiasi informazione all'oggetto, dovrai farlo attraverso le funzioni ora. A causa del modo in cui il rinnovare()
funzione funziona ora, abbiamo anche bisogno di impostare _distrutto
a vero
nel costruttore in modo che la funzione possa essere eseguita.
Con ciò, abbiamo appena adattato il nostro particella
classe di comportarsi come un IPoolable
. In questo modo, il pool di oggetti sarà in grado di creare un pool di particelle.
È giunto il momento di creare un pool di oggetti flessibile in grado di raggruppare qualsiasi oggetto che vogliamo. Questo pool agirà un po 'come una fabbrica: invece di usare il nuovo
parola chiave per creare oggetti che puoi usare, chiameremo invece un metodo nel pool che ci restituisce un oggetto.
Ai fini della semplicità, il pool di oggetti sarà Singleton. In questo modo possiamo accedervi ovunque all'interno del nostro codice. Inizia creando una nuova classe chiamata "ObjectPool" e aggiungendo il codice per renderlo un Singleton:
package public class ObjectPool private static var _instance: ObjectPool; private static var _allowInstantiation: Boolean; public static function get instance (): ObjectPool if (! _instance) _allowInstantiation = true; _instance = new ObjectPool (); _allowInstantiation = false; return _instance; public function ObjectPool () if (! _allowInstantiation) throw new Error ("Tentativo di istanziare un Singleton!");
La variabile _allowInstantiation
è il nucleo di questa implementazione Singleton: è privato, quindi solo la propria classe può essere modificata e l'unico posto dove dovrebbe essere modificato è prima di crearne la prima istanza.
Ora dobbiamo decidere come tenere le piscine all'interno di questa classe. Dal momento che sarà globale (ovvero può raggruppare qualsiasi oggetto nella tua applicazione), dobbiamo prima trovare un modo per avere sempre un nome univoco per ogni piscina. Come farlo? Ci sono molti modi, ma il meglio che ho trovato finora è quello di usare i nomi delle classi degli oggetti come nome del pool. In questo modo, potremmo avere una piscina "Particle", una piscina "nemica" e così via ... ma c'è un altro problema. I nomi delle classi devono essere unici all'interno dei loro pacchetti, quindi per esempio una classe "BaseObject" all'interno del pacchetto "nemici" e una classe "BaseObject" all'interno del pacchetto "structures" sarebbe consentita. Ciò causerebbe problemi in piscina.
L'idea di utilizzare i nomi di classe come identificatori per i pool è ancora ottima, ed è qui flash.utils.getQualifiedClassName ()
viene ad aiutarci. Fondamentalmente questa funzione genera una stringa con il nome completo della classe, inclusi eventuali pacchetti. Quindi ora possiamo usare il nome di classe qualificato di ciascun oggetto come identificatore per i rispettivi pool! Questo è ciò che aggiungeremo nel passaggio successivo.
Ora che abbiamo un modo per identificare i pool, è il momento di aggiungere il codice che li crea. Il nostro pool di oggetti dovrebbe essere abbastanza flessibile da supportare sia i pool statici che quelli dinamici (ne abbiamo parlato al punto 3, ricorda?). Dobbiamo anche essere in grado di memorizzare le dimensioni di ciascun pool e quanti oggetti attivi ci sono in ognuno. Una buona soluzione è quella di creare una classe privata con tutte queste informazioni e memorizzare tutti i pool all'interno di Oggetto
:
package 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 = new ObjectPool (); _allowInstantiation = false; return _instance; public function ObjectPool () if (! _allowInstantiation) throw new Error ("Tentativo di istanziare un Singleton!"); _pools = ; PoolInfo class public var items: Vector.; public var itemClass: Class; dimensione var pubblica: uint; public var active: uint; public var isDynamic: Boolean; funzione pubblica PoolInfo (itemClass: Class, size: uint, isDynamic: Boolean = true) this.itemClass = itemClass; items = new Vector. (dimensione,! isDynamic); this.size = size; this.isDynamic = isDynamic; attivo = 0; inizializzare(); funzione privata initialize (): void for (var i: int = 0; i < size; i++) items[i] = new itemClass();
Il codice sopra crea la classe privata che conterrà tutte le informazioni su un pool. Abbiamo anche creato il _pools
oggetto per contenere tutti i pool di oggetti. Di seguito creeremo la funzione che registra un pool nella classe:
public function registerPool (objectClass: Class, size: uint = 1, isDynamic: Boolean = true): void if (! (describeType (objectClass) .factory.implementsInterface. (@ type == "IPoolable"). length ()> 0)) throw new Error ("Impossibile creare un pool che non implementa IPoolable!"); ritorno; var qualifiedName: String = getQualifiedClassName (objectClass); if (! _pools [qualifiedName]) _pools [qualifiedName] = new PoolInfo (objectClass, size, isDynamic);
Questo codice sembra un po 'più complicato, ma non fatevi prendere dal panico. È tutto spiegato qui. Il primo Se
la dichiarazione sembra davvero strana. Potresti non aver mai visto quelle funzioni prima, quindi ecco cosa fa:
fabbrica
etichetta.implementsInterface
etichetta.IPoolable
l'interfaccia è tra di loro. Se è così, allora sappiamo che possiamo aggiungere quella classe al pool, perché saremo in grado di lanciarlo con successo come un Mi oppongo
.Il codice dopo questo controllo crea solo una voce all'interno _pools
se uno non esistesse già. Dopo quello, il PoolInfo
costruttore chiama il inizializzare()
funzione all'interno di quella classe, creando effettivamente il pool con le dimensioni che vogliamo. Ora è pronto per essere utilizzato!
Nell'ultimo passaggio siamo stati in grado di creare la funzione che registra un pool di oggetti, ma ora abbiamo bisogno di ottenere un oggetto per poterlo utilizzare. È molto semplice: otteniamo un oggetto se il pool non è vuoto e lo restituiamo. Se il pool è vuoto, controlliamo se è dinamico; se è così, ne aumentiamo le dimensioni, quindi creiamo un nuovo oggetto e lo restituiamo. In caso contrario, restituiamo null. (Puoi anche scegliere di lanciare un errore, ma è meglio restituire null e fare in modo che il tuo codice aggiri questa situazione quando succede).
Ecco il getObj ()
funzione:
funzione pubblica getObj (objectClass: Class): IPoolable var qualifiedName: String = getQualifiedClassName (objectClass); if (! _pools [qualifiedName]) throw new Error ("Impossibile ottenere un oggetto da un pool che non è stato registrato!"); ritorno; var returnObj: IPoolable; if (PoolInfo (_pools [qualifiedName]). active == PoolInfo (_pools [qualifiedName]). size) if (PoolInfo (_pools [qualifiedName]). isDynamic) returnObj = new objectClass (); PoolInfo (_pools [qualifiedName]) dimensioni ++.; PoolInfo (_pools [qualifiedName]) items.push (returnObj).; else return null; else returnObj = PoolInfo (_pools [qualifiedName]). items [PoolInfo (_pools [qualifiedName]). active]; returnObj.renew (); PoolInfo (_pools [qualifiedName]). Active ++; return returnObj;
Nella funzione, prima controlliamo che il pool esista effettivamente. Supponendo che la condizione sia soddisfatta, controlliamo se il pool è vuoto: se è, ma è dinamico, creiamo un nuovo oggetto e lo aggiungiamo nel pool. Se il pool non è dinamico, interrompiamo il codice lì e restituiamo null. Se il pool ha ancora un oggetto, otteniamo l'oggetto più vicino all'inizio del pool e chiamiamo rinnovare()
su di esso. Questo è importante: il motivo per cui chiamiamo rinnovare()
su un oggetto che era già nel pool è per garantire che questo oggetto sarà dato in uno stato "utilizzabile".
Probabilmente ti starai chiedendo: perché non usi anche questo assegno? describeType ()
in questa funzione? Bene, la risposta è semplice: describeType ()
crea un XML ogni il tempo lo chiamiamo, quindi è molto importante evitare la creazione di oggetti che usano molta memoria e che non possiamo controllare. Inoltre, solo il controllo per vedere se il pool esiste davvero è sufficiente: se la classe passata non viene implementata IPoolable
, questo significa che non saremmo nemmeno in grado di creare un pool per questo. Se non c'è una piscina per questo, sicuramente prendiamo questo caso nel nostro Se
dichiarazione all'inizio della funzione.
Ora possiamo modificare il nostro Principale
classe e usa il pool di oggetti! Controlla:
funzione privata 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; per (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 compilare e profilare l'utilizzo della memoria! Ecco cosa ho ottenuto:
È piuttosto bello, no??
Abbiamo implementato con successo un pool di oggetti che ci fornisce oggetti. È stupefacente! Ma non è ancora finita. Stiamo ancora ricevendo oggetti, ma non li restituiamo mai quando non ne abbiamo più bisogno. È ora di aggiungere una funzione per restituire gli oggetti all'interno ObjectPool.as
:
public function returnObj (obj: IPoolable): void var qualifiedName: String = getQualifiedClassName (obj); if (! _pools [qualifiedName]) throw new Error ("Impossibile restituire un oggetto da un pool che non è stato registrato!"); ritorno; var objIndex: int = PoolInfo (_pools [qualifiedName]). items.indexOf (obj); if (objIndex> = 0) if (! PoolInfo (_pools [qualifiedName]). isDynamic) PoolInfo (_pools [qualifiedName]). items.fixed = false; PoolInfo (_pools [qualifiedName]). Items.splice (objIndex, 1); obj.destroy (); PoolInfo (_pools [qualifiedName]) items.push (obj).; if (! PoolInfo (_pools [qualifiedName]). isDynamic) PoolInfo (_pools [qualifiedName]). items.fixed = true; PoolInfo (_pools [qualifiedName]). Active--;
Passiamo alla funzione: la prima cosa è controllare se c'è un pool dell'oggetto che è stato passato. Sei abituato a quel codice - l'unica differenza è che ora usiamo un oggetto invece di una classe per ottenere il nome qualificato, ma questo non cambia l'output).
Successivamente, otteniamo l'indice dell'articolo nel pool. Se non è nella piscina, lo ignoriamo. Una volta verificato che l'oggetto si trova nel pool, dobbiamo interrompere il pool nel punto in cui si trova l'oggetto e reinserire l'oggetto alla fine di esso. E perché? Poiché contiamo gli oggetti usati dall'inizio del pool, dobbiamo riorganizzare il pool per rendere tutti gli oggetti restituiti e non utilizzati alla fine di esso. E questo è quello che facciamo in questa funzione.
Per i pool di oggetti statici, creiamo a Vettore
oggetto che ha una lunghezza fissa. A causa di ciò, non possiamo splice ()
e Spingere()
oggetti indietro. La soluzione a questo è cambiare il fisso
proprietà di quelli Vettore
s a falso
, rimuovere l'oggetto e aggiungerlo alla fine e quindi modificare la proprietà in vero
. Abbiamo anche bisogno di diminuire il numero di oggetti attivi. Dopodiché, abbiamo finito per restituire l'oggetto.
Ora che abbiamo creato il codice per restituire un oggetto, possiamo fare in modo che le nostre particelle ritornino alla piscina una volta raggiunta la fine della loro vita. Dentro Particle.as
:
aggiornamento della funzione pubblica (timePassed: uint): void // Esecuzione della mossa delle particelle x + = Math.cos (_angle) * _speed * timePassed / 1000; y + = Math.sin (_angle) * _speed * timePassed / 1000; // Small easing per far sembrare il movimento piuttosto _speed - = 120 * timePassed / 1000; // Prendersi cura della durata e della rimozione _lifeTime - = timePassed; se (_lifeTime <= 0) parent.removeChild(this); ObjectPool.instance.returnObj(this);
Si noti che abbiamo aggiunto una chiamata a ObjectPool.instance.returnObj ()
lì dentro. Questo è ciò che rende l'oggetto ritornare alla piscina. Ora possiamo testare e profilare la nostra app:
E noi andiamo! Memoria stabile anche quando sono stati effettuati centinaia di clic!
Ora sai come creare e utilizzare un pool di oggetti per mantenere stabile l'utilizzo della memoria della tua app. La classe che abbiamo costruito può essere utilizzata ovunque ed è molto semplice adattare il tuo codice: all'inizio della tua app, crea pool di oggetti per ogni tipo di oggetto che vuoi raggruppare, e ogni volta che c'è un nuovo
parola chiave (che significa la creazione di un'istanza), sostituiscila con una chiamata alla funzione che ottiene un oggetto per te. Non dimenticare di implementare i metodi dell'interfaccia IPoolable
richiede!
Mantenere stabile l'utilizzo della memoria è davvero importante. Più tardi ti risparmierai un sacco di problemi nel tuo progetto quando tutto inizia a cadere a pezzi con istanze non controllate che rispondono ancora agli ascoltatori di eventi, oggetti che riempiono la memoria che hai a disposizione da usare e con il garbage collector in esecuzione e rallentano tutto. Una buona raccomandazione è quella di utilizzare sempre il pool di oggetti da ora in poi e noterai che la tua vita sarà molto più facile.
Si noti inoltre che sebbene questo tutorial fosse rivolto a Flash, i concetti sviluppati al suo interno sono globali: è possibile utilizzarlo nelle app AIR, nelle app mobili e ovunque si adatti. Grazie per aver letto!