Fiche mémoire : Alloc, retain, release, copy ? Kesako !

Bonjour à tous !

Vous n’avez jamais rencontré de problèmes avec la gestion mémoire dans votre programme ? Ca ne va pas tarder tongue

C’est pourquoi il vaut mieux s’en préoccuper en amont, plutôt que de fouiller dans votre code pour chercher la cause d’un bug.
Par ailleurs, vous remarquerez très vite qu’une erreur de gestion mémoire entraîne un nombre conséquents de bugs, tous liés !

Je précise que si vous êtes débutants, ce « tuto » va vous paraître hard… N’hésitez pas à le relire plusieurs fois et bien faire par vous-mêmes les manips proposées !

http://www.ipup.fr/forum/userimages/Image-34-1.jpg

Nous allons donc regarder ensemble les pièges à éviter, les outils à utiliser ainsi que les réflexes de codage à utiliser.

1) Mise en situation

Le développement sous iPhone se fait sans garbage collector (ramasse miettes). C’est à dire qu’il n’y a pas une « entité » qui se charge de désallouer automatiquement les objets inutiles. Il faut tout faire à la main, et bien savoir ce que l’on fait et où on va. Notez que c’est à force de pratique que vous comprendrez tous les mécanismes.

a) Qu’est-ce que la mémoire ?

Comment-ça, mon appli elle a de la mémoire ? Ca se cache où ?
En fait, lorsque vous créez un objet, ou que vous déclarez une variable, l’OS se charge de réserver des cases mémoires pour stocker les données sous formes de bits.
Je vous encourage vivement à regarder un article du site du zéro sur la mémoire.
Et comme ce site est vraiment bien fait, replongez-vous dans l’univers des pointeurs : A l’assaut des pointeurs

Voilà, adresse mémoire, valeur pointée, pointeurs de tableaux n’ont plus aucun secrets pour vous normalement !

b) Et sur mon iPhone ? 

En langage C, souvenez-vous, vous deviez gérer les allocations mémoire à la main, avec malloc, free. Cela marche toujours en Objective-C, mais rassurez-vous, cela a été simplifié, allégé !
En fait en Cocoa Touch, vous allez faire face à des termes comme « retain », « release », « copy », « alloc », « autorelease », « leaks », …
Kesako ? Et bien nous allons regarder de plus près tout cela ensemble ! Et dans la bonne humeur ! big_smile

2) Alloc, retain, copy, release, autorelease ; un vaste programme !

Commençons tout d’abord par expliquer les termes :
Chaque objet a ce que l’on appelle un « compteur de référence » (je parlerai de « retain count » de temps en temps) : c’est un nombre, positif ou nul, qui informe du nombre d’allocations d’un objet.

– retain    augmente de 1 le compteur de référence d’un objet
– release    diminue de 1 le compteur de référence d’un objet
– autorelease    diminue de 1 le compteur de référence d’un objet à un certain moment dans le futur
– alloc     alloue de la mémoire pour un objet, et le retourne avec un compteur à 1
– copy     fais une copie d’un objet et le retourne avec un compteur à 1

a) Parlons de tout (sauf autorelease)

Oui, autorelease est un mécanisme est un peu particulier, donc on regardera plus tard.

La trame de vie d’un objet est la suivante :

 Création
 Gestion mémoire
 Destruction

Création

Pour la création, il faut allouer de la mémoire en vue de stocker cet objet, puis on initialise l’état de l’objet.

MyObject *object = [[MyObject alloc] init]; // allocation mémoire puis initialisation

Gestion mémoire

Ensuite, on gére la mémoire :
On peut par exemple écrire (aucun intérêt)

MyObject *object = [[MyObject alloc] init];
// retain count à 1
[object retain];
// retain count à 2
[object release]
// retain count à 1
[object release]
// retain count à 0 -> l'objet est détruit

Maintenant, créez un projet, et créez une classe NSObject « AnObject » telle que :

AnObject.h

#import <Foundation/Foundation.h>
 
 
@interface AnObject : NSObject {
 
NSString *aString;
 
}
 
- (void) doSomething;
 
@end

AnObject.m

@implementation AnObject
 
- (id)init {
if(self = [super init])
{
aString = [[NSString alloc] initWithString:@"Toto"];
}
return self;
}
 
- (void)doSomething {
NSLog(aString);   
}
 
- (void) dealloc {
NSLog(@"%@ dealloc", [self class]);
[aString release];
[super dealloc];
}

Ensuite, dans un viewDidLoad par exemple, mettez ce code :

AnObject *object = [[AnObject alloc] init];
[object release];
 
[object doSomething];
 
[super viewDidLoad];

Lancez, vous avez ceci dans la console :

objc[71815]: FREED(id): message doSomething sent to freed object=0xd07850

Eh bien oui, on fait un release sur l’objet, que l’on détruit, puis on appelle une méthode de cet objet. Ca ne peut pas marcher ! On parle de zombie.

Par contre, si vous faites :

AnObject *object = [[AnObject alloc] init];
[object release];
 
object = nil// rajout de cette ligne
 
[object doSomething];

Vous voyez que ça ne « plante » pas. Par contre, cela ne fait rien. Pensez-y !
Le comportement normal devrait-être :

AnObject *object = [[AnObject alloc] init];
 
[object doSomething];

Avec pour résultat dans la console

–2009-08-31 14:49:15.177 LeaksBis[71910:20b] Toto

Destruction

Lorsque le retain count d’un objet atteint 0, la méthode dealloc de l’objet en question est appelée.

Appliquons

Si vous souhaitez changer aString de object, vous avez plusieurs manières de voir les choses :

Solution 1 : utiliser retain

- (void) setAString:(NSString*)newString {
if(newString != aString)
{
[aString release];
aString = [newString retain]; // on incrémente de 1 le retainCount de aString
}
}

Solution 2 : utiliser copy

- (void) setAString:(NSString*)newString {
if(newString != aString)
{
[aString release];
aString = [newString copy]; // a String a un retainCount de 1, self le possède
}
}

Mais si vous souhaitez retourner un objet fraîchement créé ?

Par exemple :

- (NSString*) returnAStringWithUpCase {
NSString *stringARetourner;
stringARetourner = [[NSString alloc] initWithFormat:@"%@ %@", aString, [aString uppercaseString]];
// release n'est jamais appelé ! -> fuite
return stringARetourner;
}

AnObject *object = [[AnObject alloc] init];
 
[object doSomething];
[object setAString:@"Coucou"];
[object setAString:@"Coucou2"];
NSLog([object returnAStringWithUpCase]);

ça marche, on a

2009-08-31 15:39:08.720 LeaksBis[72558:20b] Coucou2 COUCOU2

Cependant, on a une belle fuite : stringARetourner est créé mais jamais détruit ! Comment faire ? C’est là que l’on a besoin de l’autorelease…

b) Autorelease

On parle de autorelease pool (bassin d’autorelease) créé dans votre main :

int main(int argc, char *argv[]) {
 
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
int retVal = UIApplicationMain(argc, argv, nilnil);
[pool release];
return retVal;
}

En fait, cela fonctionne ainsi : vos objets crées sous autorelease sont mis dans une boucle. Petit à petit, le système supprime les objets de la mémoire et fait tourner la boucle.
Voici un récap d’après le cours de Stanford :

http://www.ipup.fr/forum/userimages/Image-40-1.jpg

Notez que vous êtes libre de créer un bassin en plein milieu de votre code. Par ailleurs, cela devra obligatoirement être fait si vous détachez un thread. De toute façon, la console vous le rappelera en écrivant leaks partout !

Donc, pour résoudre le problème de dessus :

- (NSString*) returnAStringWithUpCase {
NSString *stringARetourner;
stringARetourner = [[NSString alloc] initWithFormat:@"%@ %@", aString, [aString uppercaseString]];
[stringARetourner autorelease]; // Tout va bien, l'objet sera détruit un jour
return stringARetourner;
}

Attention avec l’autorelease :

Beaucoup de méthodes le cachent, par exemple :

NSString *string = [NSString stringWithFormat:@"I want %d $", 100000];
// string est sous le coup de autorelease !

Or, votre objet créé confié à autorelease va disparaître à un moment donné… Si vous souhaitez l’utiliser par la suite (par exemple :

stringAGarder = [object returnAStringWithUpCase];
 
// plus tard dans le code
NSLog(stringAGarder);

) il faut utiliser retain ou copy !
fleche

stringAGarder = [object returnAStringWithUpCase];
[stringAGarder retain];

3) Repérer les fuites

Apple fournit des outils avec son IDE XCode. Parmi ceux-ci, nous allons nous intéresser à Leaks (qui utilise Object allocations).

Pourquoi s’intéresser aux fuites ? Si vous avez une fuite d’eau sous votre évier, vous n’allez pas laisser faire et perdre de l’argent avec l’eau gaspillée + les dégâts ! Eh bien Apple c’est pareil, cela peut être un motif de refus de votre application..

Tout d’abord, créer la méthode suivante

- (void)createLeaks {
NSLog(@"createLeaks");
for(int i = 0 ; i < 100 ; i++)
{
NSMutableArray *array = [[NSMutableArray alloc] initWithCapacity:30];
}
}

Ensuite, l’appeler (ici toujours dans le viewDidLoad)

[object doSomething];
[object setAString:@"Coucou"];
[object setAString:@"Coucou2"];
NSLog([object returnAStringWithUpCase]);
stringAGarder = [object returnAStringWithUpCase];
[stringAGarder retain];
 
// il faut un release quelque part -> dealloc
 
[object release];
[self createLeaks]; // ajout de cette ligne

Lancer le programme. A priori tout va bien, pas de plantage rien…

Mais tongue

Allez dans run / start with performance tools / leaks

Une fenêtre s’ouvre

http://www.ipup.fr/forum/userimages/Image-35-2.jpg

Un peu plus tard :

http://www.ipup.fr/forum/userimages/Image-36-1.jpg

Un petit pic orange sur la ligne leaks ! Gloups :S

Cliquez sur la ligne leaks, une longue liste apparaît. Ce sont toutes vos fuites !

http://www.ipup.fr/forum/userimages/Image-37-2.jpg

Okay, mais on fait quoi ?

Cliquez sur

http://www.ipup.fr/forum/userimages/Image-39-2.jpg

cela fera apparaître un arbre généalogique qui retranscrit tous l’historique de la fuite (du début à la fin) et hop, voici le responsable :

http://www.ipup.fr/forum/userimages/Image-38-2.jpg

Un double clic sur cette case vous amène même directement sur la méthode dans votre code ! Wahou !

4) Astuces et erreurs

a) Utiliser les accesseurs

Il est bien plus commode d’utiliser les accesseurs pour gérer la mémoire de vos objets. Cela permet d’éviter un grand nombre d’erreurs.
Par exemple :

// dans .h
@property (retain) NSString *myString;
 
// dans .m
@synthesize myString
 
// dans une méthode
self.myString = aParameterStringForExample; // ici, le retain sera fait automatiquement !

N’oubliez pas que self.name équivaut à appeler une méthode

-(void)setName:(NSString*)newName

Donc

-(void)setName:(NSString*)newName {
self.name = newName;
}

équivaut à

-(void)setName:(NSString*)newName {
[self setName:newName];
}

Ca va donc boucler !

b) Attention aux array ou dictionary

En effet, ils gérent eux-même la mémoire des objets qu’il contiennent. Par exemple, pour remplir un tableau de 10 nombres, vous avez 2 solutions :

NSMutableArray *array;
int i;
// ...
for (i = 0; i < 10; i++)
{
NSNumber *n = [NSNumber numberWithInt: i];
[array addObject: n];
}

ou

NSMutableArray *array;
int i;
// ...
for (i = 0; i < 10; i++)
{
NSNumber *n = [[NSNumber alloc] initWithInt: i];
[array addObject: n];
[n release];
}

Dans le dernier cas, il n’y a aucune raison de ne pas faire le release sur n, car addObject fais automatiquement un retain.

c) Utiliser sciemment l’allocation

Prenons le cas de figure : vous avez une classe Personne comportant un prénom, un nom de famille, un âge, le sexe etc …
Vous créez une première personne de sexe féminin. Par la suite, cette personne se marie et change de nom de famille. Plutôt que de faire un release sur l’objet et ré-allouer, préférer changer le nom de famille de l’objet.

d) Vous avez un doute sur votre gestion mémoire ?
Utilisez retainCount (attention certaines classes comme NSString renvoient de drôles de valeurs…)

NSLog(@"retainCount %d", [array retainCount]);

e) Simulateur

On ne le répétera jamais assez : le simulateur est un piège ! Le comportement entre autres (ce qui nous intéresse ici) avec leaks et object allocations est très souvent complètement différent de la réalité sur device !

sources : 
cours de Stanford (leçon 3)
article sur stepwise
Memory Management in iPhone – Apple