L’utilizzo dell’oggetto UITabBarController può sollevarci da grosse problematiche relativamente ad applicazioni con oggetti UIViewControllers multipli e scambio tra le relative viste (UIView). L’utilizzo indiscriminato di questo oggetto, tuttavia, pone delle serie problematiche circa l’uso della memoria nel nostro dispositivo. Nel suo utilizzo tipico, infatti, quest’oggetto prevede l’assegnazione di un NSArray di oggetti UIViewControllers, gestendone i relativi cambi di vista e, quindi, di memoria. In quest’articolo, vedremo come rendere l’oggetto UITabBarController ed i relativi UIViewControllers ad esso legati resistenti alla carenza di memoria, offrendo un paradigma di programmazione che rispecchia quello del “lazy loading”.
UITabBarController
L’oggetto UITabBarController, permette di gestire più UIViewControllers con un sistema built-in di scambio tra le viste. Questo significa che tutti i controllori di vista saranno gestiti dal nostro UITabBarController, il quale sarà responsabile di spostare il controllo (firstResponder) e la visualizzazione (UIView) ad ognuno degli oggetti UIViewController corrispondenti a seconda della pressione sulla barra di selezione in basso.
Ogni oggetto UIViewController, nel suo metodo di istanza di inizializzazione -(id) init, può personalizzare la sua presenza sulla UITabBar, attraverso un’icona ed un testo descrittivo. Il tutto è davvero molto interessante per una rapida prototipizzazione e realizzazione di un applicativo. A titolo d’esempio, un tipico utilizzo di UITabBarController, potrebbe essere il seguente:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
//allocazione dell'oggetto UIWindow (programmatically style)
window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
//creiamo due istanze di oggetto MainViewController.
//ognuno di essi avrà una sua icona ed un suo titolo a popolare la TabBar
MainViewController *mainVC = [[MainViewController alloc] initWithTitle:@"titolo1"
andImage:@"icon1.png"
andTag:0];
MainViewController *mainVC2 = [[MainViewController alloc] initWithTitle:@"titolo2"
andImage:@"icon2.png"
andTag:1];
//inizializziamo l'oggetto UITabBarController
myTabBarController = [[UITabBarController alloc] init];
//creiamo un array autorilasciante con i riferimenti alle due istanze di oggetti MainViewController
NSArray *contenitore = [NSArray arrayWithObjects:mainVC, mainVC2, nil];
//le loro istanze, adesso, possono essere rilasciate. La responsabilità è passata all'array (retaining).
[mainVC release];
[mainVC2 release];
//assegniamo l'array alla property viewControllers del nostro oggetto UITabBarController
myTabBarController.viewControllers = contenitore;
//aggiungiamo la property view dell'oggetto UITabBarController alla UIWindow.
//in questo modo, verrà presentato a schermo il nostro oggetto UITabBarController ed il relativo
//UIViewController di default.
[window addSubview:myTabBarController.view];
[window makeKeyAndVisible];
return YES;
}
Sino a qui, nulla di nuovo (almeno spero). Il punto da cui partiremo sarà proprio questo. Come potete notare, infatti, a questo punto dell’esecuzione del nostro codice i due oggetti UIViewController (mainVC e mainVC2) sono correttamente allocati ed inizializzati; le loro istanze sono state aggiunte ad un array che, a sua volta, è stato assegnato come property all’oggetto UITabBarController.
A questo punto, il normale utilizzo di UITabBarController, prevede di preoccuparsi solo delle viste e dei relativi controllori. La loro comparsa e scomparsa dallo schermo sarà gestita, infatti, dalla barra in basso. Dal punto di vista dell’impiego della memoria, cosa succede? Da una rapida analisi, capiremo subito che, una volta apparse le relative viste a schermo, per ognuno dei controllori, queste rimarranno sempre presenti in memoria. Poco importa se ci sposteremo tra una view e l’altra; gli oggetti UIView (e tutti i sotto oggetti ad essi legati) rimarranno in memoria.
Questo comportamento, sebbene a volte potrebbe essere desiderabile, altre volte pone delle serie ipoteche sulle performance ed affidabilità della nostra applicazione.
Supponiamo che la nostra applicazione abbia 4 oggetti UIViewController, ognuno dei quali influisce sulla memoria di sistema con un peso di 4Megabytes. Supponiamo, inoltre, che il mantenere le relative viste in memoria sia assolutamente inutile ai fini del corretto funzionamento della nostra applicazione. L’utente, infatti, non ne avrebbe nessun giovamento perchè, quando è concentrato su un UIViewController, gli altri cessano di avere importanza.
L’approccio lazy loading
E’ proprio in queste circostanze che entra in gioco, un pattern di programmazione molto interessante che va sotto il nome di “lazy loading”. In cosa consiste questa tecnica di programmazione? E’ molto semplice. Si tratta di caricare i nostri oggetti (e quindi le nostre UIView e relativi sotto oggetti) solo quando serve. Da cui il nome “lazy”, “pigro”. In questo contesto, i nostri oggetti UIViewController, non caricheranno MAI oggetti non indispensabili senza una esplicita richiesta da parte dell’utente. Cerchiamo di capire meglio cosa vogliamo realizzare.
Quando scriviamo:
[window addSubview:myTabBarController.view];
Scateniamo una serie di messaggi delegati che arrivano al primo UIViewController, inserito nell’array di cui sopra. In particolare, l’aggiunta a schermo (addSubview:) di un oggetto UIView, implica il messaggio: -(void)loadView sull’oggetto target (il nostro UIViewController). Sarà quest’ultimo a creare una vista e mostrarla a schermo:
// Implement loadView to create a view hierarchy programmatically, without using a nib.
- (void)loadView {
UIView *tmpView = [[UIView alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
[tmpView setBackgroundColor:[UIColor redColor]];
myLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 320, 80)];
[myLabel setText:@"hello, world"];
[myLabel setFont:[UIFont systemFontOfSize:24]];
[myLabel setTextAlignment:UITextAlignmentCenter];
[myLabel setBackgroundColor:[UIColor clearColor]];
[tmpView addSubview:myLabel];
[myLabel release];
myField = [[UITextField alloc] initWithFrame:CGRectMake(0, 200, 320, 80)];
[myField setBackgroundColor:[UIColor yellowColor]];
[tmpView addSubview:myField];
[myField release];
myField.delegate = self;
UIButton *tmpButton = [UIButton buttonWithType:UIButtonTypeCustom];
[tmpButton setImage:[UIImage imageNamed:@"SantaClaus.jpg"] forState:UIControlStateNormal];
[tmpButton setFrame:CGRectMake(0, 100, 178, 72)];
[tmpButton addTarget:self
action:@selector(buttonPressed:)
forControlEvents:UIControlEventTouchUpInside];
[tmpView addSubview:tmpButton];
[buttonSet setObject:tmpButton forKey:@"primo"];
self.view = tmpView;
[tmpView release];
}
Le ultime due istruzioni, sono fatidiche. E’ a quel punto, infatti, che assegniamo alla property view del controllore, l’oggetto UIView creato in precedenza che, se rispetta il paradigma di retaining della memoria, sarà l’unico responsabile per tutti gli oggetti ad esso aggiunti (addSubview:). Questo vuol dire che, se eliminiamo l’oggetto dallo schermo (removeFromSuperview), con esso andranno via anche tutti i sotto oggetti. Proviamo ad estendere questo concetto.
Un watcher per la memoria
La classe UIViewController, come tutti gli oggetti che ereditano da NSObject, permette di rispondere ad un metodo molto interessante: -(void)didReceiveMemoryWarning. Questo messaggio viene lanciato dall’istanza singletone di UIApplication, ogni qualvolta si presenta una situazione di carenza di memoria all’interno del sistema. Il messaggio viene propagato a tutti gli oggetti dell’applicazione, cosichè ognuno di essi possa prendere iniziativa indipendente rispetto ad un problema di mancanza di memoria.
Sarà questo metodo a preoccuparsi per noi di “scaricare” la memoria, nel caso in cui ne avessimo bisogno durante l’esecuzione del nostro codice. Seguendo quanto ci siamo detti sopra, ci basterà implementare qualcosa di questo tipo:
- (void)didReceiveMemoryWarning {
// Releases the view if it doesn't have a superview.
[super didReceiveMemoryWarning];
if(!(self.tabBarController.selectedIndex == myTag)) {
NSLog(@"remove self.view");
[self.view removeFromSuperview];
self.view = nil;
}
}
Per ottenere un memory warning “programmaticamente”, lanciamo il nostro simulatore e scegliamo l’opzione “Simulate Memory Warning”:

Se osserviamo la nostra console, ci accorgeremo che il nostro metodo è entrato in azione. Cerchiamo di fare chiarezza su quello che sta accadendo. Ogni oggetto attualmente presente in memoria riceverà un messaggio di didReceiveMemoryWarning. A questo punto, verrà confrontato il proprio identificatore (myTag) con l’attuale UIViewController selezionato nella UITabBarController. Se i due indici coincidono, significa che l’UIViewController in esame è proprio quello attualmente mostrato a schermo, quindi non faremo nulla. Tutti gli altri UIViewController, invece, effettueranno una removeFromSuperView del proprio oggetto UIView.
Visto che rimuovere dalla superview, significa anche decrementare il contatore d’uso dell’oggetto, questo porterà a zero il retainCount degli oggetti UIView non visibili a schermo e, conseguentemente, essi verranno eliminati dalla memoria, lasciando spazio a nuovi oggetti.
Facciamo diventare pigri i nostri oggetti
All’inizio di quest’articolo vi avevo promesso che avremmo reso “pigri”, ovvero capaci di effettuare un lazy-load, i nostri oggetti. In quest’ultimo paragrafo, cercheremo di usare quanto sopra appreso a nostro vantaggio per l’obiettivo preposto.
Ogni oggetto che eredita da UIViewController, riceve tre messaggi molto importanti:
-(void)viewDidAppear:(BOOL)animated;
-(void)viewDidDisappear:(BOOL)animated;
-(void)viewWillDisappear:(BOOL)animated;
Come è evidente, la loro implementazione permette di agire nelle fasi di comparsa e scomparsa della property view del controller. Questa è un’ottima notizia per il nostro lavoro. Saranno questi messaggi, infatti, a gestire per noi il caricamento e scaricamento dinamico della memoria. Tutto quello che ci servirà fare, sarà reimplementare quanto detto sopra all’interno del metodo -(void)viewDidDisappear:(BOOL)animated:
-(void)viewDidDisappear:(BOOL)animated {
[self.view removeFromSuperview];
self.view = nil;
}
A questo punto, ogni qualvolta l’UIViewController, perderà il focus perchè l’utente ha selezionato un’altra opzione dall’oggetto UITabBarController in basso, la property view verrà rimossa dallo schermo e, con lei, tutti gli oggetti associati avranno un retainCounter pari a zero. Quindi, potranno essere purgati dalla memoria.
Alcune considerazioni
Perchè tutto funzioni è necessario legare agli indici dell’array i tag dei nostri viewcontrollers, in maniera da essere sempre sincronizzati su dove l’utente e’ posizionato. Questo viene effettuato durante la fase di inizializzazione dell’appDelegate.
Il codice sorgente allegato, illustra tutte le caratteristiche di cui sopra con un codice d’esempio funzionale. A tal proposito, provate ad inserire un testo all’interno del viewController, premendo sulla barra gialla e scrivendo qualcosa. Il risultato sarà la comparsa della nostra frase in alto al centro. Questo vuol dire che abbiamo manipolato le istanze degli oggetti presenti in memoria. Senza simulare un memory warning, provate a spostarvi tra un UIViewController e l’altro. Interessante vero? Gli oggetti continuano a mantenere lo stato precedente perchè sono ancora in memoria. Se, viceversa, effettuiamo una simulazione di memory warning, ci accorgeremo che una volta tornati sul controllore precedente, quest’ultimo avrà perso memoria dello stato e sarà stato “resettato” al suo aspetto originale. Questo avviene perchè esso viene caricato “dinamicamente” alla richiesta dell’utente (pressione sull’icona in basso dell’UITabBar). Il controllore, infatti, seguendo quanto specificato nel metodo di didReceiveMemoryWarning, scaricherà il suo contenuto ad ogni occorrenza di questo messaggio.
Se invece, implementiamo il metodo di viewDidDisappear, il risultato sarà che ad ogni perdita di focus del controllore, il suo contenuto verrà eliminato dalla memoria. Non avremo piu’, quindi, quella sorta di memoria di stato tra un viewcontroller e l’altro.
La morale della favola è: fate molta attenzione alla memoria. Averne in abbondanza non ci autorizza a sprecarla. Un occhio di riguardo ai vecchi dispositivi (iPhone 2G/3G), non potrà che aiutare tutti: utenti, ambiente, le nostre tasche.
Buon natale e…buona programmazione!
Costantino Pistagna
Se avete problemi con il progetto, questo è il nostro file di esempio.










13 Responses to “Per un pugno di Kilobytes [Gestione della memoria]”
17 Dicembre 2010
Tweets that mention Per un pugno di Kilobytes [Gestione della memoria] | devAPP -- Topsy.com[…] This post was mentioned on Twitter by Rynox, iPadWorld.it and devAPP, Bubi Devs. Bubi Devs said: Per un pugno di Kilobytes [Gestione della memoria]: L’utilizzo dell’oggetto UITabBarController può sollevarci da… http://bit.ly/fcBini […]
17 Dicembre 2010
MarcoCiao, ottimo articolo, aggiungerei che l’implementazione nel viewDidDisappear dovrebbe essere utilizzata solo in casi particolari, in quanto ad ogni cambio di viewcontroller il sistema dovrebbe ricreare tutti gli oggetti della view aumentando il carico di lavoro e andando ad inficiare un altro parametro fondamentale da centellinare di un device mobile, la durata della batteria.
18 Dicembre 2010
salvocome fate ad avere l’ios simulator?
comunque un ottimo tutorial
21 Dicembre 2010
gomoliakoNon solo… molte volte ci si trova di fronte a dover implementare dei Navigation Controller per dei tabs, cosa che renderebbe molto pericoloso la chiamata “removeFromSuperView” nella view… sarebbe una chiamata a cascata che in certi casi potrebbe essere disastrosa.
diciamo che come approccio è buono se le view non sono dentro dei navcon… ma ogni app ha le sue necessità di memoria 🙂
21 Dicembre 2010
FrancescoInteressante l’argomento però gli esempi fatti non mi sembrano azzeccati.. Il tabBarController già usa questo comportamento, ovvero appena riceve il didReceiveMemoryWarning va a togliere le view dei nostri controller che a loro volta vengono rilasciate a cascata…
Mentre fare un’operazione del genere, ovvero rilasciare la view nel viewDidDisappear mi sembra un po’ esagerato visto che alla prossima visualizzazione deve essere ricaricata!
22 Dicembre 2010
valvolineCiao Francesco, innanzitutto grazie per l’interesse nel mio articolo. Non mi sono molto chiare le ragioni che hai scritto. L’oggetto UITabBarController, infatti, non implementa (o meglio, implementa solo il comportamento standard di didReceiveMemoryWarning, implementato dal padre NSObject).
Questo significa che non c’e’ un metodo dentro UITabBarController che elimina le views dei viewcontrollers caricati nell’array. Semplicemente, quando otterrai un memory warning, che viene catchato esclusivamente dall’UITabBarController, l’unica cosa che verrà fatta, sarà un rilancio del messaggio a tutti i figli indistintamente. Se essi non sono visibili verranno eliminati.
Nel nostro esempio, volevamo spiegare COME gestire evenienze in cui sia possibile discernere in maniera personalizzata COSA eliminare, ad esempio, basandosi sul tag della UITabBar. Nello stesso senso và, l’unload preventivo in fase di disappear.
Mi trovi d’accordo sul fatto che il RICARICAMENTO può essere molto oneroso ma, alle volte, è l’unica cosa possibile se si vuole caricare un’applicazione MOLTO pesante, sfruttando UITabBarController.
Se poi, vorresti subclassare UITabBarController per far fare tutto il lavoro a lui e, quindi, sovrascrivere il suo metodo di didReceiveMemoryWarning in maniera da discernere al suo interno cosa eliminare, possiamo discutere sul design di un approccio di questo tipo.
Personalmente, ritengo che sia molto complicato (e poco scalabile) creare una soluzione di questo tipo. Se ogni UIViewController, invece, sa come eliminarsi dallo schermo, puoi evitarti di fare overriding di UITabBarController (cosa peraltro, poco consigliata anche della guidelines di Apple).
Qui ci sono due threads su stackoverflow molto interessanti che parlano un po’ di quello che ho scritto sopra. Non è compito di UITabBarController gestire l’uscita di scena (e di memoria) dei viewcontrollers.
http://stackoverflow.com/questions/1546677/uitabviewcontroller-memory-management
Ed ancora, qui parlano del fatto che il messaggio di didReceiveMemoryWarning, viene propagato a tutti gli oggetti. Tutti quelli non visibili, reagiscono con l’unloading della propria porzione di view:
http://stackoverflow.com/questions/4217462/uitabbarcontroller-deallocating-views-after-memory-warning
Spero di esserti stato utile. Ciao e grazie ancora!
22 Dicembre 2010
valvolineCiao gomoliako. In realtà, il problema del NavigationController e di removeFromSuperview, se hai implementato e pianificato correttamente i tuoi oggetti, non sarà assolutamente una cosa di cui preoccuparti.
A questo indirizzo:
http://www.iphonesmartapps.org/devapp/ClassEx_3_NavController.zip
Puoi scaricare la versione modificata con UINavigationController che, come avrai modo di vedere, effettua tutte le operazioni sopra descritte senza problemi 😉
grazie e buon lavoro!
23 Dicembre 2010
gomoliakolavoro da 3 anni con iPhone, ho iniziato con il primo iPhone e lì si che i problemi di memoria erano gatte da pelare non indifferenti 😉
adesso le cose sono un po’ più semplici, anche perchè iOS è migliorato moltissimo sotto questo aspetto, ma il fatto di pianificare bene dei NavCon perchè non abbiano eccessi in un TabCon non è sempre cosa evidente per un programmatore alle prime armi 😉
Ora, ho scaricato il tuo esempio ed ho solo modificato il metodo buttonPressed con questo:
-(void)buttonPressed:(id)sender {
MainViewController *nextController = [[MainViewController alloc] initWithTitle:@”titolo1″
andImage:@”icon1.png”
andTag:myTag * 10];
[self.navigationController pushViewController:nextController animated:YES];
[nextController release];
}
prova a caricare in tutte e due i tab un paio di view ed a simulare il memory warning e vedrai che l’app si chiuderà brutalmente 😉
era questo che intendevo dire 🙂
23 Dicembre 2010
FrancescoHo fatto un breve video per spiegare quello che volevo dire:
http://www.megaupload.com/?d=PARJE4Y2
A quanto ho capito io, l’UITabBarController rimuove le view che non sono visibili, i ViewController interessati nel didReceiveMemoryWarning controllano se la loro view è richiesta da qualcuno altrimenti la scaricano (ovvero il loro comportamento predefinito).
23 Dicembre 2010
valvolineCiao gomoliako, felice di rileggerti…aggiungerei che i tempi erano ancora peggio quando non si parlava proprio di iPhone…e c’era solo questo fantomatico XCode con quest’altro fantomatico Objective-C per programmare aqua e osx! 😀
Ho letto la tua modifica. Ovviamente, modificare a quel modo il codice, puo’ portare ad incosistenze. Ma si tratta di inconsistenze LOGICHE. Infatti, assegni alla var tag, un tag che non esiste (mytag*10) 😀
Ti accorgi di questo, se provi a fare la modifica e lanciare un memorywarning. vedrai che l’applicazione continua a resistere. Il discorso cambia se triggeri il metodo pressbutton, su entrambi i controllori. Vedrai che uno scompare e l’altro no. Questo perche’, la logica di controllo su “COSA TOGLIERE”, non è più rispettata, visto che i tag dei controllori vanno “out of bounds” rispetto all’array degli elementi (che ne ha solo due).
Ma questo, ha molto poco a che fare con la gestione della memoria. Sono problemi logici. Se rileggi il codice, vedrai che in quell’esempio, si voleva solo mostrare come accoppiare tags con viewcontrollers contenuti nell’array di TabBar.
Grazie per i commenti, buona programmazione e buone feste!!!
23 Dicembre 2010
valvolineCiao Francesco! Ottimo lavoro. viewDidUnload, viene richiamato correttamente, ma lo stesso messaggio passa a tutti i viewControllers che non sono attualemente visibili, come ti dicevo nel post precedente.
Se, per le tue esigenze, và bene rimuovere tutto indistintamente, allora e’ OK. Se, invece, vuoi rimuovere solo qualcosa in particolare, dovresti trovare un metodo un pò più arzigogolato per procedere.
Grazie!
23 Dicembre 2010
gomoliakoper me il pre-iphone era Java, dove la memoria, ahimè, è gestita dalla VM alla quale, a meno di modifiche della stessa, non è controllabile…
tornando al discorso di prima, il fatto di assegnare un tag a random era per provare a fare una cosa che ogni neofita farebbe 😀
cmq, il senso del tuo tutorial ora mi è più chiaro, puntavi ad un accoppiamento TabCon view, più che una reale gestione completa della memoria con questo controller 🙂
bom torno a lavoro prima che iTunes Connect vada in shutdown 😀
buone feste a tutti!
22 Gennaio 2011
Francesco BurelliScusate l’intromissione ma volevo solo precisare una cosa:
Esistono dei metodi precisi per la liberazione della memoria e la riallocazione “perfetta” (farò un articolo approfondito su questo).
I metodi per il problema che state discutendo sono:
– (void)loadView;
– (void)viewDidUnload;
Ovviamente sono comuni a tutti i figli di UIViewController e a UIViewController stesso!
Tutto ciò di cui fate un retain in loadView, dovreste fare un release e “=nil” nel viewDidUnload.
Non potete decidere voi quando una vista non serve più (esempio nel viewDidDisappear).
Il viewDidUnload viene chiamato subito dopo il didReceiveMemoryWarning e solo sui controller non necessari.
Immaginiamo di avere un UINavigationController di 1000 UIVIewController, appena viene lanciato il messaggio di memory warning tutti i 999 viewController ricevono viewDidUnload e rilasciano le loro viste. Appena faccio una pop sul navigationController, il 999esimo controller (avendo ricevuto viewDidUnload) prima di comparire riceverà loadView, quindi potrà ricaricare correttamente la sua view.
Lo stesso accade sul tabBarViewController