Bentrovati in questo breve articolo che illustra i principali strumenti per il debug delle nostre applicazioni per iOS.
Non credo di dover spiegare perché sia necessario fare il debug, quando scriviamo il codice siamo tutti convinti che sia corretto, poi proviamo l’applicazione e BANG… si chiude inaspettatamente…allora che fare? Mettersi seduti comodi e sereni e analizzare bene il codice che abbiamo scritto alla ricerca dell’errore. Senza tirare in ballo scuse come “Sarà un bug del sistema operativo… del compilatore… della libreria” perché, sebbene anche lì siano presenti dei bug è molto (ma molto) più probabile che il bug sia proprio nel codice che abbiamo scritto, che sembrava corretto, ma che invece non lo era affatto.
Fortunatamente Apple ha messo a disposizione dei programmatori un nutrito set di strumenti utili per effettuare il debug delle applicazioni, e altri ancora sono invece utili per effettuare il tuning delle prestazioni. Quindi niente più scuse e vediamo in dettaglio come scovare i bug nei nostri programmi.
Per fare qualche esempio ho preparato un progetto davvero pieno di bug, potete scaricarlo da QUESTO indirizzo.
Scaricate quindi ed eseguite il progetto sul simulatore: si presenterà con questa minacciosa tastiera il cui unico scopo è quello di far andare in crash l’applicazione ogni volta in un modo diverso… da questo punto di vista potremmo dire che l’applicazione non ha BUG…perché fa proprio quello per cui è stata pensata! 🙂
Primo bug
Una volta eseguito il progetto provate a cliccare sul primo pulsante, il pulsante “Unknow Selector” vedrete che l’applicazione è andata in crash e vi verrà mostrata questa schermata:

Guardiamo bene questa schermata, nella parte sinistra possiamo vedere la lista dei thread che erano in esecuzione al momento del crash, in questo caso erano 4 ed il primo (il cosiddetto “main thread”) è andato in crash.
Quello che si vede espandendo il simbolo del thread è lo stack trace, viene mostrato in ordine lo stack del thread, con i nomi dei metodi e delle funzioni.
Devo divagare per i meno esperti… Che cos’è lo stack? Vi sarete resi conto che programmando si possono annidare chiamate di funzioni dentro altre, no? Ad esempio un codice come questo:
[self.label setText:[NSString stringWithFormat:@"%@ - %d", [[stringa stringByTrimmingCharactersInSet: [NSCharacterSet whiteSpaceAndNewlineCharacterSet]];
], 40 + 2]];
Come fa il computer ad eseguire questa operazione? Se provate a farla a mente vi renderete conto che procederete a step, partendo dal setTex: ma vi fermerete subito in quanto per eseguire il setText avrete bisogno del parametro, ma per calcolare il parametro dovrete calcolare stringWithFormat, per calcolare stringWithFormat vi servirà calcolare 40 + 2, stringByTrimmingCharactersInSet e whiteSpaceAndNewlineCharacterSet.
Insomma, è necessario quindi un meccanismo che permetta di lasciare in asso l’esecuzione di una funzione per eseguire un’altra il cui risultato serve alla prima operazione… questo problema i progettisti l’hanno risolto con lo stack.
Le operazioni vengono inserite in una pila (stack in inglese) e vengono rimosse appena vengono completate così che si possa passare alla operazione sottostante.
Semplificando, per l’operazione descritta precedentemente potremmo avere uno stack come questo:

Tornando quindi al nostro debugger, sulla sinistra vediamo lo stack delle chiamate, in questo caso le nostre chiamate non sono più presenti perché sono termiante e vediamo solo le chiamate di sistema.
Nella finestra principale dove solitamente scriviamo il codice vediamo che c’è una riga evidenziata con il messaggio “Thread 1: signal SIGABRT” (vedi: http://en.wikipedia.org/wiki/SIGABRT).
Ma perché viene evidenziata questa riga e che nesso ha con il nostro bug?
La riposta è che poiché l’eccezione non è stata correttamente gestita da nessuna delle nostre classi allora è risalita via via nella gerarchia ed è arrivata al top e se anche lì nessuno la gestisce ecco che l’applicazione crassa.
Vedremo nel prossimo esempio come gestire localmente un’eccezione in modo che non risalga e faccia crashare il programma.
Nella parte bassa dello schermo vediamo la schermata che fornisce maggiori dettagli, vediamo infatti che LLDB (il debugger di XCode sostituto del buon vecchio GDB) ha stampato per noi alcuni messaggi, in particolare il secondo recita:
*** Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘-[UIRoundedRectButton setText:]: unrecognized selector sent to instance 0x6a32a40’
Ci sta dicendo, anche piuttosto chiaramente, che abbiamo provato a richiamare il metodo setText: su un oggetto di tipo UIRoundedRectButton che non ha affatto questo metodo e richiamare un metodo su un oggetto che non lo implementa è un’eccezione che fa crashare il programma.
Capita la causa non ci resta che risolvere il problema, e per risolvere il problema bisognerebbe trovare la riga di codice dove abbiamo richiamato il metodo setTex… cosa non facile su un progetto enorme.
Dobbiamo trovare una soluzione per intercettare le eccezioni non appena queste vengono sollevate, senza attendere che arrivino in cima, in questo modo potremmo vedere la riga di codice che ha generato l’eccezione e non il file main.c
Questo fortunatamente è possibile farlo ed è anche piuttosto semplice, dobbiamo aprire la scheda della gestione dei breakpoint:
Cliccare sul tasto “+” in basso a sinistra, quindi su “Add Exception Breakpoint”:

e infine su “done” assicurandosi di aver selezionato “All” per la voce Exception e “On Throw” per la voce Break.
Cosa abbiamo fatto? In pratica abbiamo aggiunto un breakpoit, che sarà attivato non appena viene sollevata un’eccezione. Ri-eseguiamo il codice e pigiamo sempre il primo bottone, vedremo questa schermata:

Ottimo, il debugger si è proprio fermato nella riga di codice incriminata! Infatti notiamo subito che viene richiamato il metodo setText: sul sender, ma il sender è un pulsante quindi il metodo corretto è setTitle:forState:
Modifichiamo quindi il metodo in questo modo e rieseguiamo il codice, vedremo che l’errore sarà sparito:
- (IBAction)unknowSelectorErrorTest:(id)sender {
[sender setTitle:@"Hello devApp" forState:UIControlStateNormal];
}
Come abbiamo detto c’è un modo per evitare che l’eccezione risalga la gerarchia del nostro programma e lo porti al crash, come si può fare?
A voler approfondire l’argomento ci starebbe tutto un intero articolo (o più di uno). È già tanta la teoria che sta dietro il design delle classi di un’applicazione e, se mettiamo in conto anche cosa dovrebbe rendersi conto che una classe ha riscontrato un errore, la faccenda si complica ancora di più.
Quello che dobbiamo fare è in pratica dire al nostro programma “ehi, prova ad eseguire questo codice, se qualcosa va storto chiamami. Ah, poi quando finisci, in ogni caso, fai queste operazioni qui…” (si lo so che tendo ad antropomorfizzare i miei programmi… lasciatemi nella mia follia) ma come si fa?
Semplicemente usando il costrutto:
@try {
<#statements#>
}
@catch (NSException *exception) {
<#handler#>
}
@finally {
<#statements#>
}
Il primo blocco contiene le operazioni da eseguire e che sono potenzialmente dannose, il blocco @catch “cattura” l’errore che è stato eventualmente sollevato dalle righe precedenti, infine il terzo blocco esegue delle istruzioni indipendentemente se ci siano stati o meno degli errori.
Se provate a guardare il codice del metodo unknowSelectorErrorManaged vedrete che in pratica è lo stesso codice “dannoso” del metodo precedente, ma stavolta può essere eseguito senza far crashare l’intera applicazione perché viene gestito localmente:
-(IBAction)unknowSelectorErrorManaged:(id)sender {
@try {
[sender setText:@"Hello devAPP"];
}
@catch (NSException *exception) {
NSLog(@"oh snap! %@", [exception description]);
}
@finally {
NSLog(@"Operazione terminata");
}
}
Questo è il messaggio di log:
Secondo bug
Vediamo adesso come lo stack ci può aiutare a trovare la fonte di un errore, eseguiamo il programma e clicchiamo sul terzo pulsante “Test stack”, il programma crasherà e verrà mostrata questa schermata:

Il programma si ferma (grazie al breakpoint) nella riga che ha scatenato l’errore ma se vogliamo avere maggiori informazioni ecco che questa volta ci viene in aiuto lo stack trace:

Si possono vedere le tre chiamate effettuate del nostro codice, l’ultima è selezionata perché è poprio lì che è avvenuto l’errore.
Cliccando sulle altre righe possiamo andare ad ispezionare il codice chiamante, così da renderci conto di qual è lo stato attuale del programma e da dove arrivano i parametri che la nostra funzione sta usando.
In questo caso ci rendiamo conto subito guardano il punto 3 che l’array che viene passato come parametro ha due sole voci, di conseguenza gli indici corretti sono 0 e 1 e non 1 e 2 come ho scritto… correggiamo l’immondo errore e verifichiamo che tutto sia corretto.
Terzo bug
Passiamo adesso al terzo errore. Se clicchiamo sul pulsante “Breakpoint condizionale ()” vedremo che tra parentesi appare un numero progressivo che fa da conta-clik… però c’è un problema: se proviamo a cliccare fino a 25 appare un numero stranissimo:

In questo caso non si tratta di un’eccezione, il programma continua a funzionare solo che non si comporta come dovrebbe. Non ci resta che piazzare un breakpoint all’interno del metodo richiamatto dal pulsante per verificare cosa succede.
Per piazzare un breakpoint clicchiamo nella barra di fianco al codice, nella riga in cui viene incrementata la variabile counter:

Così facendo l’esecuzione del programma viene bloccata ogni volta che si entra in quel metodo, ma questo può non essere molto comodo, come in questo caso in cui saremo costretti a far bloccare l’esecuzione 24 volte prima di arrivare al momento in cui vogliamo capire che cosa succede. Clicchiamo allora con il desto sul breakpoint e scegliamo “Edit breakpoint”:

e aggiungiamo la condizione:

In questo modo il breakpoint sarà attivo solo quando counter varrà 24 e avremo quindi sicuramente velocizzato tutta l’operazione.
In particolare eseguendo il debug possiamo vedere che quando counter vale 24 entra in quel brutto if (! ((int)(counter) % (int)(100 / 20 * 5)) ) assegnando poi a counter il valore si sqrt(-1)… nel dubbio che quella riga sia stata scritta sotto l’effetto dell’alcol la cancelliamo e correggiamo il codice.
Quarto bug?
Prima si salutarci lancio un test: nel programma c’è ancora un bug, chi lo trova e lo risolve posti la sua idea nei commenti!
Si conclude qui questo breve articolo sul debug con iOS, per qualsiasi domanda non esitate a chiedere sul nostro forum.
Alla prossima!
Ignazioc









6 Responses to “Corso di sopravvivenza ai crash: come fare il debug delle applicazioni iOS (iPhone e iPad)”
19 Maggio 2012
neronorxsto tentando di trovare un bug sulla mia app che crasha seguendo questi consigli,ma all’exception bp,non mi fa vedere il codice, ma il codice in linguaggio macchina:
0x1576caa: pushl %ebp <------------------------ proprio qui
0x1576cab: movl %esp, %ebp
0x1576cad: pushl %ebx
0x1576cae: pushl %edi
0x1576caf: pushl %esi
come faccio a farmi far vedere il codice sorgente e il BP dove si è fermato? proprio come nelle foto.
A sinistra ho il Thread 1,thread 3,thread 4 , thread 5 WebThread e il Thread 6.
Si p fermato su thread 1.
19 Maggio 2012
ignaziocse ti porta nel codice essembly vuol dire che non è disponibile il codice sorgente dove l’errore è stato sollevato.
O si tratta di qualche libreria oppure l’errore è generato direttamente nel l’ sdk.
Magari è un classico problema di retain/release…prova a postare il messaggio d’errore che vedi in console.
19 Maggio 2012
messi91Mitico Ignazio, dovrebbero farti un monumento. Purtroppo nonostante il tutorial (che sembra fatto a pennello per il mio problema), non sono riuscito a risolvere. Quando hai due minuti, puoi leggere il mio topic sul forum?
http://forum.devapp.it/showthread.php?4439-Errore-message-sent-to-deallocated-instance-Perch%E8
Ti sarei immensamente grato se potessi darmi una mano!
21 Maggio 2012
AlessandroGrazie mille, tutorial molto interessante
22 Maggio 2012
LucaGrazie mille per l’ottimo tutorial…..
23 Maggio 2012
AlessandroCiao Ignazio, il 4° bug è che non hai inizializzato counter prima dell’utilizzo…. giusto?
Grazie per il tutorial
Ale