MaCocoa 012

Capitolo 012 - Questioni di memoria

È stato un capitolo sofferto. Ero partito baldanzoso con l'idea del capitolo bene in testa, mi sono lanciato allegramente nella codifica e già stavo preparando le parole per scrivere il capitolo, quando è successo l'imprevisto.

L'applicazione non funzionava. Si suicidava velocemente dopo l'apertura, oppure reggeva qualche secondo, poi moriva ingloriosamente, oppure rimaneva su un bel po', per poi morire improvvisamente. Ho cominciato allora a lavorare di debugger, e niente, non riuscivo assolutamente a capire cosa stava succedendo.

Poi, l'illuminazione. Mi sono dato dello stupido (succede spesso: quando capisco dove ho sbagliato, mi do dello stupido), in quanto non avevo tenuto presente la prima causa di errori inafferrabili: l'allocazione della memoria. E quindi, in questo capitolo si parla della genesi ed apocalisse di oggetti.

Sorgenti: Documentazione Apple, varie qui e lì

Primo inserimento: 3 Dicembre 2001

Genesi: Alloc ed Init

Ho già parlato di come nasce un oggetto. Si parte dalla Classe, modello o stampo per costruire oggetti, e gli si invia il messaggio alloc. In risposta a questo messaggio, l'ambiente operativo costruisce un nuovo oggetto della classe richiesta. La costruzione di un oggetto è molto semplice: il sistema operativo individua una zona di memoria di dimensione adatta a contenerlo e mette a zero ogni locazione di memoria che ne fa parte. Dopo di che, mette a posto alcune variabili interne nascoste ai comuni mortali (la variabile d'istanza isa che collega l'oggetto alla sua classe) e restituisce un puntatore a questa zona di memoria. È questo meccanismo che spiega le due istruzioni che definiscono un nuovo oggetto:

LSMioOggetto        * ilMioOggetto ;
ilMioOggetto = [ LSMioOggetto alloc ];

È chiaro che avere tutte le variabili d'istanza a zero è molto poco utile, per cui in genere si effettua una procedura di inizializzazione, inviando un messaggio di init. Il metodo associato a questo messaggio effettua l'inizializzazione delle variabili d'istanza, assegnando loro dei valori di default. Tipicamente, quando si definisce una nuova classe, occorre scrivere un nuovo metodo init per effettuare l'inizializzazione delle nuove variabili d'istanza (ho già discusso l'argomento in uno dei capitoli precedenti).

Non è sempre necessario usare il messaggio init, ma si possono utilizzare altri messaggi con degli argomenti; l'importante è in ogni caso chiamare all'interno del metodo init il metodo init della superclasse, in modo da effettuare correttamente le inizializzazioni della gerarchia delle classi.

Apocalisse: release

Quando un oggetto non serve più, lo si butta via. Per aprire il sacco della spazzatura, si invia all'oggetto il messaggio di release. Ecco quindi la corretta sequenza d'uso di un oggetto:

LSMioOggetto        * ilMioOggetto ;
ilMioOggetto = [[ LSMioOggetto alloc ] init ];
// uso e abuso dell'oggetto
[ ilMioOggetto release ];

E fin qui, è tutto molto semplice e chiaro. La regola base da seguire è che chi costruisce un oggetto, è responsabile della sua vita e della sua eliminazione quando non è più necessario.

Ma supponiamo adesso che un dato oggetto sia utilizzato da due diversi enti software (altri due oggetti, ad esempio), ognuno ignaro dell'altro. Ad esempio pensiamo ad un database che contiene informazioni su dei file. Una di queste informazioni è l'icona del file, che è un'oggetto esso stesso. Creando un elemento del database principale, devo creare non solo l'oggetto che conserva le informazioni del file, ma anche l'oggetto immagine che contiene l'icona. Se adesso inserisco un nuovo file con la stessa icona, verosimilmente inserisco un nuovo oggetto file, ma utilizzo l'oggetto immagine precedente per l'icona. Ora, se cancello le informazioni (l'oggetto) del primo file, non posso buttare via anche l'immagine, che è utilizzato dall'altro oggetto. D'altra parte, questo secondo oggetto non sa che il primo oggetto non c'è più, quindi, quando cancellato, non può cancellare l'immagine, non sapendo se qualcun altro la usa.

Il metodo per risolvere questo problema è molto semplice: si tratta di contare quanti stanno usando un determinato oggetto. Esiste allo scopo una variabile d'istanza di NSObject (che è quindi ereditata da tutti gli oggetti Cocoa), della quale, in piena logica Object-Oriented, non è noto il nome ma solo come si usa. Con il messaggio retainCount è quindi possibile leggere tale variabile (la chiamo io contavita), mentre per modificarne il valore esistono diversi meccanismi.

La vita in un contatore

L'uso della variabile contavita semplifica la gestione degli oggetti; a questo punto non ci si deve più preoccupare di chi gestisce gli oggetti condivisi, ma si lascia fare al sistema operativo. All'interno dell'ambiente operativo c'è un meccanismo che va in giro con una falce: di ogni oggetto che incontra chiede il valore del contatore interno. Se la variabile contavita è zero, nessuno sta usando quell'oggetto. La sua vita non ha più significato, e quindi verrà eliminato brutalmente inviandogli un messaggio di dealloc (che è il contrario di alloc).

Per evitare che la triste signora con la falce ci tolga da sotto il naso gli oggetti in uso, è quindi bene gestire correttamente la variabile contavita.

In primo luogo, contavita è incrementata di uno quando l'oggetto è creato dal messaggio alloc, oppure attraverso un'operazione di copia.

Esiste poi il messaggio retain, che incrementa di uno contavita. Userò retain ogni volta che l'oggetto che ho sottomano deve continuare a vivere almeno finché ne ho bisogno. Chiamo l'operazione ritenuta: inviare il messaggio retain significa ritenere l'oggetto.

Quando ho terminato di usarlo, invio il messaggio di release, che diminuisce di uno la variabile contavita. Chiamo l'operazione rilascio: inviare il messaggio release significa rilasciare l'oggetto. All'interno del metodo che realizza release, dopo l'operazione di decremento si controlla il valore della variabile. Se è zero, invia all'oggetto il messaggio di dealloc.

Nella gestione di questa variabile sta il problema che mi ha fatto perdere un bel po' di tempo. Ecco il pezzo di codice incriminato.

Ho una classe che ha come variabile d'istanza un oggetto (non preoccupatevi dei nomi che non sapete, non sono essenziali alla comprensione); il file .h mostra le righe:

@interface LSDataSource : NSObject
{
        NSMutableArray        *listaFile ;
}

Poiché bisogna inizializzare la variabile listaFile alla creazione dell'oggetto LSDataSource, ho riscritto il metodo init come segue:

- (id ) init
{
        self = [ super init ] ;
        listaFile = [ NSMutableArray array ] ;
        return self ;
}

Semplice e lineare, ma soprattutto sbagliato; lancio infatti l'applicazione in esecuzione, e quello che ottengo è un errore tragico:

mc012.app has exited due to signal 10 (SIGBUS).

Dove sta il problema?

Sta nel fatto che l'oggetto restituito dall'espressione

[ NSMutableArray array ]

rimane vivo finche si trova all'interno del metodo init. Appena si esce dal metodo, l'oggetto puntato da listaFile perde ogni significato, e la triste signora con la falce lo ha eliminato dall'esistenza. Ma quell'oggetto in realtà serve al resto dell'applicazione, e serve vivo. Ecco quindi che aggiungendo un messaggio di retain, tutto funziona meravigliosamente:

- (id ) init
{
        self = [ super init ] ;
        listaFile = [ NSMutableArray array ] ;
        [ listaFile retain ];
        return self ;
}

Questo meccanismo funziona, ma non è corretto. C'è un metodo migliore per scrivere il tutto, e che risolve anche un ulteriore problema, che illustro con un esempio.

Devo creare un oggetto, diciamo una stringa per fissare le idee: ho un metodo che restituisce un puntatore alla stringa appena creata.

- (NSString *) faiStringa
{
        NSString * str ;
        str = [ [ NSMutableString alloc] init ] ;
        // altre operazioni sulla stringa
        return str ;
}

Semplice e lineare, ed ancora una volta sbagliato. La stringa è contata in uso una volta di troppo, in quanto il contavita della stringa esce da qui col valore di uno. Dovrebbe invece uscire col valore nullo, perché nessuno (all'uscita) la sta (ancora) usando. Inviare un messaggio esplicito di release prima dell'istruzione di return è addirittura peggio, perché al release l'oggetto raggiunge un contavita nullo, e viene immediatamente ucciso. Il metodo restituisce un puntatore ad una zona di memoria che contiene spazzatura; l'applicazione, qualche millisecondo dopo, morirà per certo inviando qualche messaggio strano.

Esiste quindi un ulteriore messaggio, chiamato autorelease, che ha gli stessi effetti di release (diminuisce di uno il contavita), ma lo fa più tardi, quando tutti coloro che avevano bisogno di quell'oggetto hanno finito.

Ora, vi ricordo che un'applicazione con interfaccia utente funziona più o meno così: si parte, si inizializzano le variabili interne, poi si aspetta che succeda qualcosa. Quando succede qualcosa (un evento), si gestisce l'evento, poi si torna ad aspettare. Questo infinito attendere un evento è chiamato il loop degli eventi. Il meccanismo degli autorelease funziona come un bidone della spazzatura, e la triste signora con la falce diventa più prosaicamente l'addetto della nettezza urbana che passa periodicamente a portare via la spazzatura. All'inizio del loop degli eventi si crea un nuovo bidone vuoto. Poi l'applicazione attende un evento. Quando arriva un evento, sarà eseguita una operazione più o meno corposa di codice, con creazione e distruzione di oggetti. Gli oggetti che devono essere distrutti non lo sono subito, ma sono buttati dentro il bidone, e lì rimangono, ancora vivi, fino alla fine delle operazioni conseguenti all'evento. Alla fine, quando l'evento è stato processato, passa il netturbino e svuota il bidone. Poi, si ricomincia con un nuovo bidone.

Assegnare una variabile

Voglio concludere il capitolo con i metodi chiamati accessor, ovvero i metodi che permettono di accedere alle variabili d'istanza. Consideriamo un metodo per assegnare il valore ad una variabile d'istanza che sia essa stessa un oggetto. La prima realizzazione è sbagliata:

- (void)setListaFile:(NSMutableArray*)newListaFile
{
        [listaFile release];
        listaFile = [newListaFile retain];
}

Eppure, sembra tutto a posto: butto via il vecchio oggetto e mantengo un puntatore al nuovo oggetto. A questo oggetto, per di più, gli dico di sopravvivere perché ne ho appunto bisogno. A prima vista, è corretto; ma consideriamo cosa succede se gli oggetti listaFile e newListaFile sono in realtà lo stesso oggetto. Prima butto via l'oggetto, poi faccio per prendere quello nuovo, e non c'è più.

Ecco la versione corretta:

- (void)setListaFile:(NSMutableArray*)newListaFile
{
        if (listaFile != newListaFile) {
                [listaFile release];
                listaFile = [newListaFile retain];
        }
}

Prima di fare qualsiasi cosa, verifico se gli oggetti incriminati sono differenti. Se lo sono, procedo come descritto, altrimenti, non ho nulla da fare!

Utilizzare il metodo accessor è proprio il metodo migliore per gestire l'assegnazione di una variabile d'istanza. Il metodo ha da sé i pregi dell'incapsulamento (non mi preoccupo della struttura dati nemmeno all'interno dell'oggetto stesso!), della comprensione e delle facilità di manutenzione. Ecco quindi la versione definitiva del metodo init:

- (id ) init
{
        self = [ super init ] ;
        [self setListaFile: [ NSMutableArray array ]];
        return self ;
}

Riassunto finale

Riassumendo e ricapitolando questo capitolo piuttosto tecnico ma fondamentale:

Licenza Creative Commons
Eccetto dove diversamente specificato, i contenuti di questo sito sono rilasciati sotto Licenza Creative Commons.
Pagina a cura di Livio Sandel (macocoa2012@gmail.com).