Oggi parliamo di gestione della memoria nelle applicazioni iPhone (e iPad). Dopo un primo ottimo articolo sull’argomento, creato dal nostro Ignazio Calò, abbiamo pensato fosse meglio, viste le numerose richieste, spendere ancora un paio di parole su questo tema di vitale importanza per lo sviluppo di applicazioni per dispositivi mobili come appunto iPhone, iPod Touch e iPad.
Perchè è così importante la gestione della memoria?
Gestire la memoria significa prima di tutto evitare di sprecare inutilmente risorse (in questo caso di memoria), che, soprattutto nel caso dei dispositivi mobili, ritroviamo in quantità limitata e non espandibile. Ovviamente gestire la memoria non è solo importante per questo tipo di device, ma anche per le applicazioni desktop, le cui funzionalità potrebbero portare ad occupare enormi quantità di memoria (pensate ad esempio ad applicazioni di tipo scientifico). Quanto mostreremo in questo articolo sarà utile quindi non solo per lo sviluppo di applicazioni iPhone e iPad, ma anche per quello di applicazioni MacOS.
Come saprete (o comunque intuirete) ogni variabile, puntatore, oggetto o altro elemento del nostro programma occupa memoria e anche se potrebbero sembrarvi quantità minime, sappiate che l’apparenza, spesso, inganna. Perchè dico questo? Objective-C (il linguaggio principale con cui lavoriamo) è un linguaggio orientato agli oggetti, e questa potente tecnica di programmazione, come sapete, ci offre funzionalità potentissime, ma allo stesso tempo molto pericolose se non capiamo a fondo il loro principio di funzionamento. Una di queste potenti (e pericolose) caratteristiche è l’ereditarietà, ovvero la caratteristica che ci permette di creare nuovi oggetti personalizzati, partendo da altri esistenti mantenendo tutte le carattaristiche (proprietà e metodi) dell’oggetto di partenza.
Ora, come ben saprete la maggior parte del nostro lavoro su iPhone e iPad si basa sul framework Cocoa Touche in particolare dall’UIKit, offerto da Apple, (maggiori informazioni qui) e come potete vedere dal seguente schema esiste una gerarchia specifica per gli oggetti di questo utile kit, in particolare notate come tutti gli oggetti siano derivati di un unico “super” oggetto: NSObject.

Ora immaginiamo un semplice caso, dobbiamo creare una nuova classe con metodi ed eventi personalizzati, che eredita direttamente da NSObject. Analizzando il file di interfaccia della nostra nuova classe, potremmo trovarci di fronte a qualcosa di simile:
@interface MyClass: NSObject {
IBOutlet UITextField *myTextField;
}
// Eventuali metodi
@end
Istanziato un oggetto di questa classe, come minimo dovremo aver sufficiente spazio in memoria da allocare per contenere un oggetto UITextField, ma non solo. Dato che questa classe eredita da NSObject, dobbiamo aver spazio sufficiente per contenere anche tutti gli elementi di questo oggetto!
Immaginate ora di creare un nuovo oggetto partendo invece da UITextField, in questo caso dovrete prevedere di aver spazio in memoria per i nuovi elementi e, secondo lo schema sopra, anche per gli elementi di UITextField (da cui ereditate direttamente), ma anche quelli di UIControl (da cui eredita UITextField), UIView (da cui eredita UIControl), UIResponder (da cui eredita UIView) e infine NSObject. Insomma, come potrete intuire le cose si complicano un pochino e di fatto, se pensavate di cavarvela con poca memoria, in realtà ci rendiamo conto che ne serve più del previsto e dovremo allocare la quantità di memoria necessaria a contenere sia i nuovi elemente da noi creati che tutti quelli ereditati. Oltre ad allocare la quantità corretta di memoria, capirete quindi che sarà il caso, inoltre, di liberare appena possibile la memoria utilizzata, ovvero quando l’oggetto residente in memoria non serve più.
Insomma, un attenta pianificazione e gestione della memoria può essere decisiva per il successo della vostra applicazione iPhone!
Allocare la giusta quantità di memoria per un oggetto in Objective-C (alloc, init)
Allocare la corretta quantità di memoria (ricordate ne che abbiamo una quantità limitata da usare), sembrerebbe quindi un processo lungo e complesso, ma non è così. Il framework, e in particolare proprio il “super” oggetto NSObject, ci offre infatti un metodo di classe molto utile: Alloc!
Grazie a questo metodo (che potremo usare nei nostri oggetti che ereditano direttamente o indirettamente da NSObject) saremo in grado di allocare la giusta quantità di memoria sia per tutti i nostri nuovi elementi. che per quelli ereditati. A livello di codice è molto semplice, se dovessimo creare nel nostro programma un’istanza di MyClass, questo potrebbe essere il codice da scrivere:
MyClass *myNewClass = [MyClass alloc];
L’istruzione è ancora incompleta, infatti dobbiamo ancora inizializzare il nostro oggetto perchè sia pronto per essere utilizzato all’interno del nostro programma. Per farlo possiamo sfruttare un altro metodo offerto da NSObject: init!
Grazie ad “init” potremo appunto inizializzare i nostri oggetti, ovvero dotarlo della configurazione di default che servirà all’avvio dell’applicazione (o meglio al momento della creazione dell’oggeto). Anche qui il codice è molto semplice:
MyClass *myNewClass = [[MyClass alloc] init];
Unica cosa che dobbiamo ricordare è che come lo spazio da allocare deve essere calcolato anche per gli elementi degli oggetti da cui si eredita, anche l’inizializzazione deve essere fatta per questi stessi elementi. Niente paura, anche per questo ci basterà un’unica riga di codice all’interno del metodo init nell’implementazione della nostra classe:
- (id)init {
[super init];
// Codice per inizializzazione
return self;
}
Grazie all’istruzione [super init] non ci dovremo preoccupare di inizializzate tutto manualmente.
A questo punto il nostro oggetto “myNewClass ” è pronto per essere utilizzato.
Liberare la memoria al momento giusto (retain, release, dealloc)
Per la creazione del nostro oggetto nel programma è tutto, è infatti stata creata un’istanza della nostra classe, la abbiamo allocata in memoria (considerando lo spazio necessario) e infine la abbiamo inizializzata a dovere per poterla utilizzare come meglio crediamo. Ovviamente la gestione della memoria non si limita a questo, dovremo infatti prevedere, ad un certo punto, di liberare lo spazio di memoria occupato dalla stessa. NSObject offre un metodo che compie questo lavoro: dealloc, questo, però, non dovrà mai essere chiamato esplicitamente, ma dovrà essere chiamato indirettamente dal metodo di protocollo release. Non preoccupatevi se non capite a fondo cosa sto dicendo, ora vedrò di spiegarvi il meccanismo che sta dietro questa frase.
Perchè non possiamo invocare direttamente dealloc? Semplicemente perchè deallocando l’oggetto sparirebbe dall’intero programma e potrebbe capitare che in realtà, questo, serva ancora da qualche parte. Infatti oltre a servire nel metodo in cui è chiamato, un oggetto potrebbe essere passato come argomento ad altro oggetto e avere ancora uno scopo che necessiti la sua esistenza. Deallocandolo in un caso come questo il programma restituirebbe un errore o comunque si comporterebbe in modo anomalo e imprevisto.
Da qui nasce l’esigenza avere un contatore che indichi al nostro programma se esiste ancora qualcuno interessato ad sfruttare il nostro oggetto. Questo contatore dovrà avere valore 1 non appena l’oggetto verrà allocato in memoria, dovrà quindi essere incrementato di 1 ogni volta che un altro oggetto ha interesse a mantenerlo in memoria, e diminuito di 1 quando un oggetto decide che non gli serve più.
Anche in questo caso non dovremmo inventarci nulla in quanto in nostro aiuto arriva il nostro caro NSObject, che offre ad ogni oggetto da esso ereditato una proprietà chiamata retainCount. Quando retainCount varrà 0, dealloc verrà richiamato in automatico e la memoria sarà liberata.
Come detto allocando un oggetto in memoria verrà impostato il suo retainCount a 1, noi dovremo semplicemente dire al programma di sottrargli un 1 quando non ci servirà più, per farlo useremo il metodo release:
MyClass *myNewClass = [[MyClass alloc] init];
// Facciamo qualcosa con la nostra myNewClass
[myNewClass release];
In particolare riportiamo i momenti in cui retainCount può variare (tratti dal nostro precedente articolo su questo tema a cura di Ignazio Calò):
- Quando una variabile viene inizializzata il suo retainCount viene incrementato di 1.
- I metodi alloc, copy, allocWithZone incrementano retainCount di 1.
- I metodi release, autorelease decrementano retainCount di 1. (autorelease lo fa in un momento non deterministico)
- Se non diversamente specificato il metodo autorelease viene invocato di default.
Casi particolari
Ora, senza entrare troppo nel dettaglio, sappiate che esistono alcuni casi in cui possiamo usare un oggetto senza allocarlo o deallocarlo, semplicemente il tutto viene gestito in automatico e gli oggetti vengono rilasciati quando non servono più.
Vediamo un esempio:
NSString *myNewString = [NSString stringWithFormat:@"Nuova stringa"];
In questo caso stiamo utilizzando il metodo di classe “stringWithFormat” (quelli che iniziano con + per intenderci), per questo tipo di metodi non dovremo preoccuparci di nulla e potremo utilizzare questi oggetti senza troppi pensieri.
Errori nella gestione della memoria
Per concludere vediamo cosa comporta non gestire correttamente la memoria durante lo sviluppo di un’applicazione iPhone, iPad e anche Mac. Se allochiamo un oggetto, dedichiamo quindi ad esso uno spazio in memoria, ma ci dimentichiamo per qualche ragione di rilasciarlo, questo occuperà per tutta la durata dell’esecuzione del programma quello spazio in memoria, e ogni volta che richiameremo una nuova istanza dell’oggetto, un altro pezzettino di memoria andrà occupato inutilmente. Capirete che pian piano, la memoria disponibile (ricordiamo che sui dispositivi mobili è limitata in modo considerevole rispetto agli ambienti desktop), andrà ad esaurirsi fino al blocco del programma stesso o al verificarsi di comportamenti anomale e imprevisti. Questo tipo di errore nella gestione della memoria è chiamato Memory Leak.
Per farvi comprendere meglio cosa significa incappare in questo errore vi riportiamo, per semplicità e comodità, e vista inoltre l’immediatezza e la chiarezza di quanto illustrato, un breve esempio tratto da wikipedia:
Questo esempio vuole dimostrare come un leak può nascere, ed i suoi effetti, senza dover conoscere le basi della programmazione. Questo è solo un esempio fittizio.
Il programma in questione fa parte di un software molto semplice dedicato al controllo di un ascensore. Questa porzione di algoritmo viene eseguita ogni volta che qualcuno all’interno preme un bottone.
Quando il bottone viene premuto:
- recupera un po’ di memoria per ricordare il piano richiesto
- metti il numero richiesto in memoria
- siamo già al piano giusto?
- se sì, non dobbiamo fare niente: finito
- altrimenti, aspetta finché l’ascensore è disponibile
- vai al piano richiesto
- rilascia la memoria usata per ricordare il numero del piano
Questo programma potrebbe sembrare corretto, ma contiene un leak. Consideriamo il caso in cui l’ascensore si trova al piano 3 e premiamo il bottone 3. Otteniamo un po’ di memoria che non restituiremo mai. Ogni volta che succede perdiamo un po’ di memoria.
Questo problema non avrà un effetto immediato. Le persone non premono il bottone di un piano su cui stanno, ed in ogni caso ci può essere abbastanza memoria da gestire questa situazione centinaia o migliaia di volte. Ma alla fine la memoria finirà. Potrebbe richiedere mesi o anni, o potrebbe non essere mai scoperto.
Le conseguenze potrebbero essere sgradevoli; alla fine l’ascensore smetterebbe di funzionare. Se il programma avesse bisogno di memoria per aprire le porte, qualcuno potrebbe restare intrappolato all’interno, visto che non abbiamo risorse per aprirle.
Bisogna notare che il memory leak aumenta finché il programma è in esecuzione. Ad esempio, se un calo di elettricità blocca l’ascensore, al ritorno dell’alimentazione la memoria sarà completamente disponibile ed il lento processo di perdita di memoria deve ricominciare da zero.
Questo articolo non può certo considerarsi esaustivo sull’argomento, ma speriamo di aver tolto qualche dubbio in più e di invogliarvi, vista l’importanza, ad approfondire questo “apparentemente noioso” argomento.
7 Responses to “L#012 – Gestione della memoria durante lo sviluppo di applicazioni iPhone e iPad”
14 Luglio 2010
lucagrazie, ottimo tutorial.
avrei una domanda:
il metodo init del vostro esempio:
– (id)init {
[super init];
// Codice per inizializzazione
return self;
}
che in questo caso non fa nulla di nuovo rispetto alla classe da cui eredita
e’ proprio necessario usarlo?
Voglio dire, se non lo metto nella mia sub-classe (facendo una sorta di override del metodo della classe da cui eredito) verra’ chiamato in automatico il metodo della superclasse, o ho capito male io?
grazie ancora
14 Luglio 2010
Staff devAPPNell’esempio mostrato, MyClass eredita direttamente da NSObject, che di fatto non inizializza nulla, (quindi si può anche evitare) nonostante questo male non fa e conviene quindi abituarsi ad usarlo sempre per tutte le nostre sottoclassi 😉
14 Luglio 2010
El JobsoBhe… Interessante ma serv apparentemente solo per app “grandi” tipo quelle della EA, Gameloft ecc… anche se si puo applicare anche a app piccole! Grazie comunque! 🙂
P.S = Aspetto ansiosamente il tutorial sulla registrazione vocale; lo dovrò applicare alla mia app! Grazie anche per questo! 🙂
14 Luglio 2010
Mokka77In realtà tutte le applicazioni, anche le più banali devono godere di un’ottima gestione della memoria, non solo quelle grandi! Oltre a problemi di risorse si può incappare in comportamenti inaspettati. Non sottovalutare questo argomento!!
15 Luglio 2010
FiliQuesto argomento è tanto affascinatne quanto complicato, la gestione della memoria è utilissima, padroneggiare la memoria è fondamentale per uno sviluppatore, peccato che come tutto, anche questa padronanza si apprende con l’esperienza, peccato che per me ancora la strada è lunga, dovrei studiarmi bene questo argomento
15 Luglio 2010
NoyaLeggo sempre volentieri gli articoli sulla gestione della memoria perchè, malgrado tutto, faccio sempre un pò di confusione. In particolar modo vorrei capire un pò meglio come comportarsi con le variabili definite come @property(retain). In questo caso come facciamo a deallocarle? Non ho capito se sia sufficiente porli = nil nel metodo dealloc o sia da chiamare [self.unaProperty release].
Infine vi chiederei, se non c’è già (non mi sembra di averlo visto), se aveste tempo e voglia di spiegare un pò bene come funziona gli instrument Leak e Allocations (perfomance tool) per monitorare l’esecuzione dell’applicazione.
Grazie e complimenti per questo sito!
17 Luglio 2010
DavideSono al 100% d’accordo con Mokka77. Conviene fare attenzione a gestire la memoria in modo corretto anche con applicazioni leggere.
Si e’ vero, se sono leggere una gestione poco corretta puo’ avere meno conseguenze a livello utente. Il problema viene amplificato con il passare del tempo pero’. Se usi l’applicazione per un periodo di tempo prolungato il rischio di crashes sale, e anche se leggera, l’app puo’ terminare involontariamente.
Per quanto riguarda la gestione delle variabili definite con @property(retain)….
Bisogna prima di tutto capire che retain crea un setter che manda un messagio di retain all’oggetto. Per esempio:
self.oggetto = [[Classes alloc] init];
questa linea di codice crea un oggeto che avra’ un retain count = 2;
(alloc -> +1, self. -> +1)
questo perche’ la proprieta’ e’ di tipo “retain”.
Se quindi vuoi essere in grado di diallocare “oggetto” devi prima portare il suo retain count a 0, dove e come ti serve…
Dopo di che puoi settare oggetto nil.
E’ un argomento che richiederebbe piu tempo per spiegarlo in modo profondito, ma spero questo ti possa aiutare.