
Quando si parla di linguaggio di programmazione Objective-C, abbreviato spesso con Obj-c, una tra le prime informazioni che viene data è che l’intero linguaggio è un superset del linguaggio standard C. Ciò significa in breve che all’interno di un programma obj-c potete scrivere tutto il codice C che vi pare, questo funzionerà perfettamente. Questo permette, tra l’altro, di poter riutilizzare moltissime librerie “classiche” scritte in C all’interno dei nostri programmi, come le socket BSD e molto altro.
Obj-C ha un’altra particolarità degna di nota, che va sotto il nome di Binding Dinamico (wikipedia). Il Binding è il processo che viene eseguito dal compilatore o dall’interprete che associa una particolare parte di codice al nome di una funzione. È, in altri termini, quel processo che ci permette di invocare una parte di codice utilizzando il nome della funzione associata.
In Obj-C questo processo viene eseguito a runtime, viene cioè fatto durante l’esecuzione del programma stesso e prende il nome di Binding Dinamico, mentre negli altri linguaggi viene effettuato a compile time, ovvero durante la fase di compilazione. In quest’ultimo caso si chiama Binding statico.
Il binding in C
Il binding è un argomento piuttosto tecnico ma per capire il concetto alla base vediamo due esempi in C, in questo primo programma viene semplicemente richiamata la funzione doppio() quindi niente di particolare. (eseguite il codice qui: http://ideone.com/l4KIYx)
#include
int doppio(int n) {
return n * 2;
}
int main(void) {
int input = 42;
int risultato = doppio(input);
printf("Il risultato: %d", risultato);
return 0;
}
In questo caso il compilatore “semplicemente” sostituisce alla chiamata alla funzione doppio() il suo indirizzo di memoria.
In questo secondo esempio abbiamo invece un puntatore a funzione, che viene assegnato alla funzione doppio() o triplo() a seconda se il valore è pari o dispari. (esegui il codice qui: http://ideone.com/IkYy9B)
#include
int doppio(int n) {
return n * 2;
}
int triplo (int n) {
return n * 3;
}
int main(void) {
int k = 3;
int risultato;
int (*func)(int k);
if (k % 2 == 0) {
func = doppio;
} else {
func = triplo;
}
risultato = func(k);
printf("Il risultato: %d", risultato);
return 0;
}
Quello che si vede in questo esempio è che usando i puntatori a funzione possiamo, a runtime, switchare il comportamento della funzione tra una implementazione o un’altra. Possiamo, cioè, con lo stesso nome invocare parti di codice completamente distinte.
Questo ragionamento un po’ articolato è indispensabile se si vuole scrivere codice ad oggetti con un linguaggio che nativamente non è ad oggetti ed è infatti alla base del funzionamento dell’Obj-c.
In Obj-c tutta la parte che si occupa del binding è inserita in una libreria che si chiama “Objective-C runtime” e della quale potete leggere qui (http://goo.gl/Tc2y3j) la documentazione e qui (http://goo.gl/fd1U6A) la programming guide.
Questa libreria è molto importante anche per altri argomenti che vanno oltre lo scopo di questo articolo, come l’introspection e la metaprogrammazione.
Objc_msgsend
Cosa succede dietro le quinte del linguaggio Obj-C quando viene inviato un messaggio? Cosa fa di preciso una semplice riga come questa?
id returnvalue = [anobject messageName:parameter];
Il compilatore traduce questa riga in puro C e traducendo l’invio del messaggio in una chiamata alla funzione objc_msgSend() il cui prototipo è questo:
id objc_msgSend(id theReceiver, SEL theSelector, ...)
Questa “traduzione” produce questa riga di codice
id returnvalue = objc_msgsend(anobject, @selector(messageName:), parameter);
Per trovare la corretta implementazione del selector messageName l’obj-c runtime analizza la mappa dei metodi dell’oggetto anobject. Questa mappa contiene da una parte il selector (che è una struct C) e dall’altra un puntatore a funzione.
Se il runtime non trova il selector all’interno di questa mappa allora segue il puntatore “isa” dell’oggetto per risalire nella gerarchia delle classi.
Se per esempio invochiamo il metodo alloc sulla classe NSString, nella sua mappa non ci sarà alcuna entry per questo selector e in runtime proseguirà nella ricerca sulla classe NSObject che è l’immediata superclass di NSString.
Swizzling
La tecnica dello swizzling constite nell’invertire a runtime l’implementazione di due metodi ed è quindi possibile soltanto negli ambienti con il binding dinamico.
Obj-c si presta bene a questo tipo di tecnica che, anche se è un po’ “oscura”, si può rivelare molto utile in certi casi.
Create un progetto xcode di qualsiasi tipo, in questo caso io ho utilizzato “console application” perché non ho bisogno di alcuna interfaccia per questo esercizio.
All’interno del progetto create una classe con due metodi:
//file Dog.h
#import
@interface Dog : NSObject
-(NSString *)bark;
-(NSString *)growl;
@end
//file Dog.m
#import "Dog.h"
@implementation Dog
-(NSString *)bark {
return @"WOOF";
}
-(NSString *)growl {
return @"GRRRR";
}
@end
Se avete creato un progetto di tipo console andate a modificare il file main, viceversa utilizzate il vostro delegate.
La prima cosa da fare è verificare il comportamento della nostra classe, quindi scriviamo un piccolo esempio all’interno del main
int main(int argc, const char * argv[])
{
@autoreleasepool {
Dog *dog1 = [[Dog alloc] init];
NSLog(@"Bark: %@", [dog1 bark]);
NSLog(@"Growl: %@", [dog1 growl]);
}
}
l’output sarà senza grosse sorprese:
2013-09-07 09:52:40.646 learnSwizzling[95797:303] Bark: WOOF 2013-09-07 09:52:40.649 learnSwizzling[95797:303] Growl: GRRRR
Adesso però facciamo la magia! usiamo due metodi dell’obj-c runtime per mischiare le carte 🙂
Primo passo importiamo la libreria con un:
#import
Poi aggiungiamo sotto il codice scritto poco fa il codice per scambiare le due implementazioni:
Method original, swizzled;
original = class_getInstanceMethod([Dog class], @selector(bark));
swizzled = class_getInstanceMethod([Dog class], @selector(growl));
method_exchangeImplementations(original, swizzled);
a questo punto possiamo ri-verifica il comportamento della nostra classe, stampando nuovamente le righe di log. Ecco il codice completo della mia funzione main():
#import
#import
#import "Dog.h"
int main(int argc, const char * argv[])
{
@autoreleasepool {
Dog *dog1 = [[Dog alloc] init];
NSLog(@"Bark: %@", [dog1 bark]);
NSLog(@"Growl: %@", [dog1 growl]);
Method original, swizzled;
original = class_getInstanceMethod([Dog class], @selector(bark));
swizzled = class_getInstanceMethod([Dog class], @selector(growl));
method_exchangeImplementations(original, swizzled);
NSLog(@"Bark: %@", [dog1 bark]);
NSLog(@"Growl: %@", [dog1 growl]);
}
return 0;
}
Adesso l’output sarà questo:
2013-09-07 09:52:40.646 learnSwizzling[95797:303] Bark: WOOF 2013-09-07 09:52:40.649 learnSwizzling[95797:303] Growl: GRRRR 2013-09-07 09:52:40.650 learnSwizzling[95797:303] Bark: GRRRR 2013-09-07 09:52:40.651 learnSwizzling[95797:303] Growl: WOOF
dove si nota chiaramente che le ultime due invocazioni hanno cambiato il loro comportamento.
Conclusioni
Conoscere e utilizzare in modo produttivo l’obj-c runtime può aiutare molto nello sviluppo di applicazioni complesse, ma occhio a come viene usato! Invertire l’implementazione di alcuni metodi o agire così in profondità può portare ad avere un codice dal comportamento imprevedibile e quindi impossibile da debuggare, quindi usatelo solo quando strettamente necessario.
Chi conosce l’argomento UnitTest (qui una introduzione) probabilmente utilizza anche qualche framework per gestire i mock object, come OCMock oppure OCMokito. Alla base del funzionamento di questi framework c’è proprio l’obj-c runtime senza il quale non si potrebbero compiere tutte le modifiche a basso livello necessarie per generare i mock object.
Per chi volesse approfondire l’argomento del method swizzling consiglio la lettura di questo post: http://stackoverflow.com/a/8636521
Alla prossima!
No Responses to “Uno sguardo al runtime Objective-c”